DOM-Based XSS:基於 DOM 的跨站腳本攻擊

最隱蔽的 XSS - 攻擊發生在客戶端

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


📚 本篇重點

  • 🎯 理解 DOM-based XSS 的獨特性
  • 🔍 識別危險的 JavaScript API
  • 🛡️ 學習前端防禦策略
  • 💼 掌握實際案例與檢測方法

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


1️⃣ 什麼是 DOM-based XSS?

📖 定義

DOM-based XSS 是一種特殊的 XSS 攻擊,攻擊完全發生在客戶端(瀏覽器),惡意 Payload 從未發送到伺服器,而是通過操作 DOM(Document Object Model)直接在瀏覽器中執行。

🔑 關鍵差異

特徵Reflected/Stored XSSDOM-based XSS
攻擊位置伺服器端 → 客戶端🔴 純客戶端
Payload 傳遞HTTP 請求 → 響應🔴 URL Fragment (#)
伺服器記錄✅ 有記錄🔴 無記錄(# 後不發送)
檢測難度較易🔴 困難
防禦層伺服器 + 客戶端🔴 只能在客戶端
WAF 有效性有效🔴 無效

🔄 攻擊流程對比

傳統 XSS (Reflected/Stored):

1. 攻擊者發送惡意 Payload
   ↓
2. 伺服器處理請求(可能記錄)
   ↓
3. 伺服器將 Payload 嵌入響應
   ↓
4. 瀏覽器接收響應
   ↓
5. 瀏覽器執行惡意腳本

DOM-based XSS:

1. 攻擊者構造惡意 URL(含 # fragment)
   ↓
2. 受害者點擊 URL
   ↓
3. 瀏覽器載入頁面(# 後內容不發送到伺服器!)
   ↓
4. JavaScript 讀取 URL fragment
   ↓
5. JavaScript 不安全地操作 DOM
   ↓
6. 惡意腳本執行

🌟 生活比喻

想像一個智能保險箱:

傳統 XSS:

  • 你告訴保全:「請幫我在保險箱裡放一張紙,上面寫著『炸彈💣』」
  • 保全檢查(或不檢查)後放入
  • 你打開保險箱,紙條出現,炸彈爆炸

DOM-based XSS:

  • 保險箱有一個顯示屏,顯示「密碼提示」
  • 攻擊者在密碼提示中注入:「你的密碼是 [炸彈💣]」
  • 保全從未看到這個提示(因為是保險箱內部顯示的)
  • 你打開保險箱,看到提示,炸彈爆炸

關鍵點: DOM-based XSS 的攻擊繞過了伺服器的檢查,直接在「保險箱內部」(客戶端)發生。


2️⃣ DOM-based XSS 的特殊性

為什麼 URL Fragment (#) 不會發送到伺服器?

// URL: http://example.com/page?param=value#fragment

// 伺服器收到:
// GET /page?param=value HTTP/1.1

// 伺服器 **看不到** #fragment 部分!
// 這部分只存在於瀏覽器中

HTTP 請求示例:

# 完整 URL
http://example.com/search?q=test#section=results

# 實際發送到伺服器的請求
GET /search?q=test HTTP/1.1
Host: example.com

# #section=results 永遠不會出現在伺服器日誌中!

這就是為什麼:

  • ❌ 伺服器端 XSS 過濾器無效
  • ❌ WAF(Web Application Firewall)無效
  • ❌ 伺服器日誌中無記錄
  • ✅ 只能依賴客戶端防禦

3️⃣ 危險的 JavaScript API

Source(來源):用戶可控的輸入

// 1. URL 相關
location.hash          // #fragment
location.search        // ?query=value
location.href          // 完整 URL
document.URL           // 完整 URL
document.documentURI   // 完整 URI
document.baseURI       // Base URI

// 2. Referrer
document.referrer      // 來源頁面 URL

// 3. 表單輸入
document.forms[0].elements[0].value

// 4. Cookie
document.cookie

// 5. WebMessage
window.postMessage     // 跨窗口通信

// 6. LocalStorage/SessionStorage
localStorage.getItem('key')
sessionStorage.getItem('key')

Sink(接收器):危險的 DOM 操作

// 1. 直接執行代碼(最危險)
eval(userInput)                    // 🔴 極度危險
setTimeout(userInput, 1000)        // 🔴 極度危險
setInterval(userInput, 1000)       // 🔴 極度危險
Function(userInput)()              // 🔴 極度危險
new Function(userInput)()          // 🔴 極度危險

// 2. HTML 注入
element.innerHTML = userInput      // 🔴 危險
element.outerHTML = userInput      // 🔴 危險
document.write(userInput)          // 🔴 危險
document.writeln(userInput)        // 🔴 危險

// 3. 屬性操作
element.src = userInput            // 🟠 中等危險(javascript:)
element.href = userInput           // 🟠 中等危險(javascript:)
element.setAttribute('onclick', userInput)  // 🔴 危險

// 4. jQuery
$(userInput)                       // 🔴 危險(會執行選擇器)
$('#element').html(userInput)      // 🔴 危險
$('#element').append(userInput)    // 🔴 危險

// 5. 導航
location = userInput               // 🟠 中等危險
location.href = userInput          // 🟠 中等危險
location.replace(userInput)        // 🟠 中等危險

4️⃣ 實際攻擊案例

案例 1: 基礎 innerHTML 注入

❌ 危險代碼

<!DOCTYPE html>
<html>
<head>
    <title>搜尋結果</title>
</head>
<body>
    <h1>搜尋結果</h1>
    <div id="results"></div>

    <script>
    // 從 URL fragment 讀取搜尋關鍵字
    // URL: http://example.com/search#q=<script>alert('XSS')</script>

    function displayResults() {
        // 🔴 危險:直接從 location.hash 讀取並寫入 innerHTML
        var hash = location.hash.substring(1);  // 移除 #
        var params = new URLSearchParams(hash);
        var query = params.get('q');

        // 🔴 危險:innerHTML 會執行腳本
        document.getElementById('results').innerHTML =
            '<h2>搜尋: ' + query + '</h2>';
    }

    displayResults();
    </script>
</body>
</html>

攻擊 URL:

http://example.com/search#q=<img src=x onerror="alert(document.cookie)">

效果: 彈出 Cookie


✅ 安全代碼

<!DOCTYPE html>
<html>
<head>
    <title>搜尋結果</title>
</head>
<body>
    <h1>搜尋結果</h1>
    <div id="results"></div>

    <script>
    function displayResults() {
        var hash = location.hash.substring(1);
        var params = new URLSearchParams(hash);
        var query = params.get('q');

        // ✅ 方法 1:使用 textContent(不會執行 HTML)
        var h2 = document.createElement('h2');
        h2.textContent = '搜尋: ' + query;
        document.getElementById('results').appendChild(h2);

        // ✅ 方法 2:手動 HTML escape
        // var escaped = escapeHtml(query);
        // document.getElementById('results').innerHTML =
        //     '<h2>搜尋: ' + escaped + '</h2>';
    }

    function escapeHtml(text) {
        var map = {
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&quot;',
            "'": '&#x27;',
            "/": '&#x2F;',
        };
        return text.replace(/[&<>"'\/]/g, function(m) { return map[m]; });
    }

    displayResults();
    </script>
</body>
</html>

案例 2: eval() 執行

❌ 危險代碼

<!DOCTYPE html>
<html>
<head>
    <title>計算器</title>
</head>
<body>
    <h1>簡易計算器</h1>
    <div id="result"></div>

    <script>
    // URL: http://example.com/calc#expr=2+2
    function calculate() {
        var hash = location.hash.substring(1);
        var params = new URLSearchParams(hash);
        var expr = params.get('expr');

        // 🔴 極度危險:eval 會執行任意代碼
        var result = eval(expr);

        document.getElementById('result').textContent =
            '結果: ' + result;
    }

    calculate();
    </script>
</body>
</html>

攻擊 URL:

http://example.com/calc#expr=alert(document.cookie)

效果: 執行 alert(document.cookie)


✅ 安全代碼

<!DOCTYPE html>
<html>
<head>
    <title>計算器</title>
</head>
<body>
    <h1>簡易計算器</h1>
    <div id="result"></div>

    <script>
    function calculate() {
        var hash = location.hash.substring(1);
        var params = new URLSearchParams(hash);
        var expr = params.get('expr');

        // ✅ 方法 1:使用白名單驗證
        if (!/^[\d\+\-\*\/\(\)\s]+$/.test(expr)) {
            document.getElementById('result').textContent =
                '錯誤:不允許的字符';
            return;
        }

        // ✅ 方法 2:使用安全的計算庫(如 math.js)
        try {
            // var result = math.evaluate(expr);  // 使用 math.js

            // 或手動解析表達式
            var result = safeEval(expr);

            document.getElementById('result').textContent =
                '結果: ' + result;
        } catch (e) {
            document.getElementById('result').textContent =
                '錯誤:無效的表達式';
        }
    }

    function safeEval(expr) {
        // 簡單的安全計算(僅支援基本運算)
        // 更好的方案:使用專門的數學表達式解析庫
        var allowed = /^[\d\+\-\*\/\(\)\s]+$/;
        if (!allowed.test(expr)) {
            throw new Error('Invalid expression');
        }
        // 使用 Function 而非 eval(稍微安全一些)
        return new Function('return ' + expr)();
    }

    calculate();
    </script>
</body>
</html>

案例 3: jQuery 選擇器注入

❌ 危險代碼

<!DOCTYPE html>
<html>
<head>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
    <div id="content">
        <div class="item" data-id="1">項目 1</div>
        <div class="item" data-id="2">項目 2</div>
        <div class="item" data-id="3">項目 3</div>
    </div>

    <script>
    // URL: http://example.com/page#id=1
    function highlightItem() {
        var hash = location.hash.substring(1);
        var params = new URLSearchParams(hash);
        var itemId = params.get('id');

        // 🔴 危險:jQuery 會執行選擇器中的 HTML
        var selector = '.item[data-id="' + itemId + '"]';
        $(selector).css('background-color', 'yellow');
    }

    highlightItem();
    </script>
</body>
</html>

攻擊 URL:

http://example.com/page#id=1"]<img src=x onerror=alert(1)>

構造的選擇器:

'.item[data-id="1"]<img src=x onerror=alert(1)>"]'

jQuery 會解析並執行 <img> 標籤!


✅ 安全代碼

<!DOCTYPE html>
<html>
<head>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
    <div id="content">
        <div class="item" data-id="1">項目 1</div>
        <div class="item" data-id="2">項目 2</div>
        <div class="item" data-id="3">項目 3</div>
    </div>

    <script>
    function highlightItem() {
        var hash = location.hash.substring(1);
        var params = new URLSearchParams(hash);
        var itemId = params.get('id');

        // ✅ 方法 1:不使用字符串拼接選擇器
        $('.item').each(function() {
            if ($(this).attr('data-id') === itemId) {
                $(this).css('background-color', 'yellow');
            }
        });

        // ✅ 方法 2:使用 filter
        $('.item').filter(function() {
            return $(this).data('id') == itemId;
        }).css('background-color', 'yellow');

        // ✅ 方法 3:驗證 itemId 格式
        if (!/^\d+$/.test(itemId)) {
            console.error('Invalid item ID');
            return;
        }
        var selector = '.item[data-id="' + itemId + '"]';
        $(selector).css('background-color', 'yellow');
    }

    highlightItem();
    </script>
</body>
</html>

案例 4: location.href 注入

❌ 危險代碼

<!DOCTYPE html>
<html>
<head>
    <title>重定向</title>
</head>
<body>
    <h1>正在重定向...</h1>

    <script>
    // URL: http://example.com/redirect#url=https://google.com
    function redirect() {
        var hash = location.hash.substring(1);
        var params = new URLSearchParams(hash);
        var url = params.get('url');

        if (url) {
            // 🔴 危險:允許 javascript: 協議
            location.href = url;
        }
    }

    // 3 秒後重定向
    setTimeout(redirect, 3000);
    </script>
</body>
</html>

攻擊 URL:

http://example.com/redirect#url=javascript:alert(document.cookie)

效果: 3 秒後執行 alert(document.cookie)


✅ 安全代碼

<!DOCTYPE html>
<html>
<head>
    <title>重定向</title>
</head>
<body>
    <h1>正在重定向...</h1>

    <script>
    function redirect() {
        var hash = location.hash.substring(1);
        var params = new URLSearchParams(hash);
        var url = params.get('url');

        if (url) {
            // ✅ 驗證 URL 格式
            if (!isValidUrl(url)) {
                console.error('Invalid URL');
                document.body.innerHTML = '<h1>錯誤:無效的 URL</h1>';
                return;
            }

            location.href = url;
        }
    }

    function isValidUrl(url) {
        try {
            var urlObj = new URL(url);

            // ✅ 只允許 http/https 協議
            if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') {
                return false;
            }

            // ✅ 可選:白名單域名
            var allowedDomains = ['example.com', 'trusted-site.com'];
            if (!allowedDomains.some(domain => urlObj.hostname.endsWith(domain))) {
                console.warn('URL not in whitelist');
                // 根據需求決定是否拒絕
            }

            return true;
        } catch (e) {
            return false;
        }
    }

    setTimeout(redirect, 3000);
    </script>
</body>
</html>

5️⃣ 真實案例

案例 1: Google Maps DOM XSS (2013)

漏洞細節:

  • Google Maps 使用 location.hash 讀取地圖參數
  • 沒有正確驗證就用 innerHTML 顯示

攻擊向量:

https://maps.google.com/#<img src=x onerror=alert(1)>

修復: Google 實施了嚴格的輸入驗證和輸出編碼


案例 2: AngularJS 沙箱繞過

漏洞細節:

  • AngularJS 1.x 的表達式沙箱可以被繞過
  • 攻擊者可以通過 URL fragment 注入惡意表達式

攻擊 URL:

http://example.com/app#{{constructor.constructor('alert(1)')()}}

影響: 所有使用 AngularJS 1.x 的應用

修復: AngularJS 移除了表達式沙箱,改用 CSP


案例 3: Gmail DOM XSS (2015)

漏洞細節:

  • Gmail 的附件預覽功能存在 DOM XSS
  • 通過 postMessage 注入惡意代碼

修復: Google 強化了 postMessage 驗證


6️⃣ 防禦策略

防禦原則

1. 驗證所有來自 Source 的輸入
   ↓
2. 避免使用危險的 Sink
   ↓
3. 使用安全的 API
   ↓
4. 實施 CSP
   ↓
5. 使用 Trusted Types(新標準)

安全的 JavaScript 編碼實踐

1. 使用安全的 DOM API

// ❌ 危險
element.innerHTML = userInput;
document.write(userInput);

// ✅ 安全
element.textContent = userInput;  // 只設置文字,不解析 HTML
element.innerText = userInput;    // 類似 textContent

// ✅ 安全:建立元素
var div = document.createElement('div');
div.textContent = userInput;
container.appendChild(div);

2. HTML Escape

/**
 * HTML Escape 函數
 */
function escapeHtml(text) {
    var map = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#x27;',
        '/': '&#x2F;',
    };
    return String(text).replace(/[&<>"'\/]/g, function(m) {
        return map[m];
    });
}

// 使用
var safeText = escapeHtml(userInput);
element.innerHTML = '<div>' + safeText + '</div>';

3. URL 驗證

/**
 * 驗證 URL 安全性
 */
function isValidUrl(url) {
    try {
        var urlObj = new URL(url);

        // 只允許 http/https
        if (!['http:', 'https:'].includes(urlObj.protocol)) {
            return false;
        }

        // 可選:域名白名單
        var trustedDomains = [
            'example.com',
            'trusted-site.com'
        ];

        var isAllowed = trustedDomains.some(function(domain) {
            return urlObj.hostname === domain ||
                   urlObj.hostname.endsWith('.' + domain);
        });

        return isAllowed;
    } catch (e) {
        return false;
    }
}

// 使用
var redirectUrl = getUrlParam('url');
if (isValidUrl(redirectUrl)) {
    location.href = redirectUrl;
} else {
    console.error('Invalid URL');
}

4. 避免 eval 和類似函數

// ❌ 絕對不要使用
eval(userInput);
setTimeout(userInput, 1000);
setInterval(userInput, 1000);
Function(userInput)();
new Function(userInput)();

// ✅ 使用替代方案
// 如果需要動態執行,使用白名單
var allowedFunctions = {
    'functionA': functionA,
    'functionB': functionB
};

var funcName = getUserInput();
if (allowedFunctions.hasOwnProperty(funcName)) {
    allowedFunctions[funcName]();
}

5. 安全地使用 jQuery

// ❌ 危險:jQuery 會執行選擇器
$(userInput);
$('#element').html(userInput);
$('#element').append(userInput);

// ✅ 安全:使用 text()
$('#element').text(userInput);

// ✅ 安全:建立元素後再操作
var $div = $('<div>').text(userInput);
$('#container').append($div);

// ✅ 安全:明確指定上下文
$(document.getElementById('user-element')).text(userInput);

Django + JavaScript 整合範例

# views.py
from django.shortcuts import render
from django.utils.html import escapejs
import json

def search_page(request):
    # 從後端傳遞安全配置到前端
    config = {
        'apiEndpoint': '/api/search',
        'maxResults': 10,
        'allowedDomains': ['example.com', 'trusted.com']
    }

    return render(request, 'search.html', {
        'config_json': json.dumps(config)
    })
<!-- templates/search.html -->
<!DOCTYPE html>
<html>
<head>
    <title>搜尋</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <h1>搜尋</h1>
    <input type="text" id="searchBox" placeholder="輸入關鍵字...">
    <button onclick="search()">搜尋</button>
    <div id="results"></div>

    <script>
    // ✅ 從後端接收配置(已經過 JSON 編碼)
    var config = {{ config_json|safe }};

    function search() {
        var query = document.getElementById('searchBox').value;

        // ✅ 驗證輸入
        if (!query || query.length > 100) {
            alert('請輸入有效的搜尋關鍵字');
            return;
        }

        // ✅ 使用 fetch 而非直接操作 DOM
        fetch(config.apiEndpoint + '?q=' + encodeURIComponent(query))
            .then(function(response) { return response.json(); })
            .then(function(data) {
                displayResults(data.results);
            })
            .catch(function(error) {
                console.error('Error:', error);
            });
    }

    function displayResults(results) {
        var container = document.getElementById('results');

        // ✅ 清空容器
        container.textContent = '';

        // ✅ 安全地建立元素
        results.forEach(function(result) {
            var div = document.createElement('div');
            div.className = 'result-item';

            var title = document.createElement('h3');
            title.textContent = result.title;  // ✅ textContent 不會執行 HTML

            var description = document.createElement('p');
            description.textContent = result.description;

            div.appendChild(title);
            div.appendChild(description);
            container.appendChild(div);
        });
    }

    // ✅ 安全地處理 URL fragment
    function handleHashChange() {
        var hash = location.hash.substring(1);

        if (!hash) return;

        var params = new URLSearchParams(hash);
        var query = params.get('q');

        if (query) {
            // ✅ 驗證長度
            if (query.length > 100) {
                console.error('Query too long');
                return;
            }

            // ✅ 設置到輸入框(textContent 自動 escape)
            document.getElementById('searchBox').value = query;
            search();
        }
    }

    // 監聽 hash 變化
    window.addEventListener('hashchange', handleHashChange);
    handleHashChange();  // 初始載入時執行
    </script>
</body>
</html>

使用 CSP 防禦

# settings.py (Django)

# 使用 django-csp
MIDDLEWARE = [
    # ...
    'csp.middleware.CSPMiddleware',
]

# CSP 配置
CSP_DEFAULT_SRC = ("'none'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'",)
CSP_IMG_SRC = ("'self'", "data:", "https:")
CSP_CONNECT_SRC = ("'self'",)

# 🔑 關鍵:禁用 eval 和 inline scripts
# 'unsafe-eval' 和 'unsafe-inline' 都不要使用!

# 如果必須使用 inline script,使用 nonce
CSP_INCLUDE_NONCE_IN = ['script-src']
<!-- 使用 nonce -->
{% load csp %}
<script {% csp_nonce %}>
    // Inline script with nonce
    console.log('This is safe with nonce');
</script>

Trusted Types (新標準)

<!DOCTYPE html>
<html>
<head>
    <!-- 啟用 Trusted Types -->
    <meta http-equiv="Content-Security-Policy"
          content="require-trusted-types-for 'script'">
</head>
<body>
    <div id="content"></div>

    <script>
    // 建立 Trusted Types 策略
    if (window.trustedTypes && trustedTypes.createPolicy) {
        var escapePolicy = trustedTypes.createPolicy('escape', {
            createHTML: function(string) {
                // 實施 HTML escape
                return string
                    .replace(/&/g, '&amp;')
                    .replace(/</g, '&lt;')
                    .replace(/>/g, '&gt;')
                    .replace(/"/g, '&quot;')
                    .replace(/'/g, '&#x27;');
            }
        });

        // 使用 Trusted Types
        var userInput = getUserInput();
        var trustedHTML = escapePolicy.createHTML(userInput);
        document.getElementById('content').innerHTML = trustedHTML;
    }
    </script>
</body>
</html>

7️⃣ 檢測工具

手動檢測

// dom-xss-checker.js
/**
 * 檢查頁面中的 DOM XSS 漏洞
 */
(function() {
    console.log('開始 DOM XSS 檢查...');

    // 1. 檢查危險的 Source
    var sources = {
        'location.hash': location.hash,
        'location.search': location.search,
        'location.href': location.href,
        'document.URL': document.URL,
        'document.referrer': document.referrer
    };

    console.log('Sources:');
    for (var key in sources) {
        console.log('  ' + key + ':', sources[key]);
    }

    // 2. 檢查是否使用了危險的 Sink
    var originalInnerHTML = Object.getOwnPropertyDescriptor(
        Element.prototype, 'innerHTML'
    ).set;

    Object.defineProperty(Element.prototype, 'innerHTML', {
        set: function(value) {
            console.warn('⚠️  innerHTML 被設置:', value);
            console.trace();  // 顯示調用堆棧
            return originalInnerHTML.call(this, value);
        }
    });

    // 3. 檢查 eval
    var originalEval = window.eval;
    window.eval = function(code) {
        console.error('🔴 eval 被調用:', code);
        console.trace();
        return originalEval.call(this, code);
    };

    console.log('DOM XSS 檢查已啟動');
})();

自動化掃描

# 1. Burp Suite Professional
# DOM XSS Scanner extension

# 2. OWASP ZAP
# DOM XSS Scanner plugin

# 3. DOM Invader (Burp Suite extension)
# 自動檢測 DOM XSS

# 4. eslint 規則
npm install --save-dev eslint-plugin-no-unsanitized

# .eslintrc.js
module.exports = {
    plugins: ['no-unsanitized'],
    rules: {
        'no-unsanitized/method': 'error',
        'no-unsanitized/property': 'error'
    }
};

8️⃣ 面試常見問題

Q1: DOM-based XSS 和 Reflected XSS 的主要差異是什麼?

參考答案:

核心差異:

  1. 攻擊位置:

    • Reflected XSS: 伺服器端 → 客戶端
    • DOM-based XSS: 純客戶端
  2. Payload 傳遞:

    • Reflected XSS: 通過 HTTP 請求參數(?param=value)
    • DOM-based XSS: 通過 URL Fragment(#fragment)
  3. 伺服器可見性:

    • Reflected XSS: 伺服器可以看到並記錄 Payload
    • DOM-based XSS: # 後的內容不會發送到伺服器
  4. 防禦層:

    • Reflected XSS: 可在伺服器端防禦
    • DOM-based XSS: 只能在客戶端防禦
  5. WAF 有效性:

    • Reflected XSS: WAF 可以檢測和阻擋
    • DOM-based XSS: WAF 無效(Payload 未發送到伺服器)

實例:

// Reflected XSS
// URL: http://example.com/search?q=<script>alert(1)</script>
// 伺服器看到 ?q=<script>alert(1)</script>

// DOM-based XSS
// URL: http://example.com/search#q=<script>alert(1)</script>
// 伺服器看不到 #q=<script>alert(1)</script>

Q2: 為什麼 eval() 很危險?應該用什麼替代?

參考答案:

eval() 的危險性:

  1. 執行任意代碼:
eval(userInput);  // 用戶可以執行任何 JavaScript 代碼!
// 攻擊者輸入: "alert(document.cookie)"
// 結果: Cookie 被竊取
  1. 繞過所有防護:
// 即使你以為輸入是安全的...
var userInput = "2+2; alert(document.cookie)";
var result = eval(userInput);  // 執行了 alert!
  1. 性能問題: eval 需要調用 JavaScript 解釋器,執行速度慢

替代方案:

1. 使用 JSON.parse (解析 JSON):

// ❌ 危險
var data = eval('(' + jsonString + ')');

// ✅ 安全
var data = JSON.parse(jsonString);

2. 使用數學表達式庫:

// ❌ 危險
var result = eval('2+2*3');

// ✅ 安全:使用 math.js
var result = math.evaluate('2+2*3');

3. 使用白名單:

// ❌ 危險
var funcName = getUserInput();
eval(funcName + '()');

// ✅ 安全:白名單
var allowedFunctions = {
    'save': saveData,
    'load': loadData,
    'delete': deleteData
};

var funcName = getUserInput();
if (allowedFunctions.hasOwnProperty(funcName)) {
    allowedFunctions[funcName]();
}

4. 使用策略模式:

// ❌ 危險
var operation = getUserInput();  // "add", "subtract", etc.
var result = eval(operation + '(a, b)');

// ✅ 安全
var operations = {
    'add': function(a, b) { return a + b; },
    'subtract': function(a, b) { return a - b; },
    'multiply': function(a, b) { return a * b; },
    'divide': function(a, b) { return a / b; }
};

var operation = getUserInput();
if (operations.hasOwnProperty(operation)) {
    var result = operations[operation](a, b);
}

關鍵原則: 永遠不要用 eval 處理用戶輸入


Q3: 如何安全地處理 location.hash 中的用戶輸入?

參考答案:

完整的安全處理流程:

/**
 * 安全處理 location.hash
 */
function safeHandleHash() {
    // Step 1: 讀取 hash
    var hash = location.hash.substring(1);  // 移除 #

    if (!hash) return;

    // Step 2: 解析參數
    var params = new URLSearchParams(hash);
    var userInput = params.get('q');

    if (!userInput) return;

    // Step 3: 驗證輸入
    // 3.1 長度限制
    if (userInput.length > 100) {
        console.error('Input too long');
        return;
    }

    // 3.2 格式驗證(根據實際需求)
    // 例如:只允許字母數字和空格
    if (!/^[a-zA-Z0-9\s]+$/.test(userInput)) {
        console.error('Invalid characters');
        return;
    }

    // Step 4: 安全地輸出到 DOM
    // ✅ 方法 1:使用 textContent
    document.getElementById('output').textContent = userInput;

    // ✅ 方法 2:如果必須使用 innerHTML,先 escape
    var escaped = escapeHtml(userInput);
    document.getElementById('output').innerHTML =
        '<div class="result">' + escaped + '</div>';
}

/**
 * HTML Escape
 */
function escapeHtml(text) {
    var div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// 監聽 hash 變化
window.addEventListener('hashchange', safeHandleHash);
safeHandleHash();  // 初始載入

常見錯誤:

// ❌ 錯誤 1:直接使用 innerHTML
var hash = location.hash.substring(1);
element.innerHTML = hash;  // 危險!

// ❌ 錯誤 2:不驗證就使用
var hash = location.hash.substring(1);
element.textContent = hash;  // textContent 雖安全,但沒驗證

// ❌ 錯誤 3:黑名單過濾
var hash = location.hash.replace(/<script>/g, '');  // 可繞過!
element.innerHTML = hash;

// ✅ 正確:白名單驗證 + 安全 API
var hash = location.hash.substring(1);
if (/^[a-zA-Z0-9]+$/.test(hash)) {  // 白名單
    element.textContent = hash;  // 安全 API
}

額外建議:

  1. 使用 CSP 禁用 inline scripts
  2. 使用 Trusted Types
  3. 實施長度和格式驗證
  4. 記錄異常輸入
  5. 定期安全審計

9️⃣ 重點回顧

核心概念

  1. DOM-based XSS 的獨特性:

    • 攻擊完全發生在客戶端
    • 使用 URL Fragment(#)傳遞 Payload
    • 伺服器無法檢測和記錄
    • WAF 無效
  2. 危險的組合:Source + Sink:

    • Source: location.hash, location.search, document.referrer
    • Sink: innerHTML, eval(), setTimeout(), document.write()
  3. 防禦策略:

    • ✅ 使用安全的 API(textContent 而非 innerHTML)
    • ✅ 驗證所有來自 Source 的輸入
    • ✅ 避免 eval 和類似函數
    • ✅ 實施 CSP
    • ✅ 使用 Trusted Types
  4. JavaScript 安全編碼:

    • 永遠不要用 eval 處理用戶輸入
    • 使用 textContent 而非 innerHTML
    • 驗證 URL 協議(只允許 http/https)
    • HTML Escape 所有用戶輸入

安全檢查清單

代碼審查:

  • 搜尋所有 location.hash / location.search 使用
  • 搜尋所有 innerHTML / outerHTML 使用
  • 搜尋所有 eval() / Function() / setTimeout(string) 使用
  • 搜尋所有 document.write() 使用
  • 檢查 jQuery $() / .html() / .append() 使用

防禦措施:

  • 所有 Source 輸入都經過驗證
  • 使用安全的 DOM API
  • 實施 HTML Escape
  • 實施 CSP(禁用 unsafe-eval 和 unsafe-inline)
  • 使用 Trusted Types(如果瀏覽器支援)
  • 配置 eslint-plugin-no-unsanitized

測試:

  • 手動測試常見 Payload
  • 使用自動化工具掃描(Burp Suite, ZAP)
  • Code Review 重點檢查 Source-Sink 路徑

📖 延伸閱讀


🔗 系列導航


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

0%