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 XSS | DOM-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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
"/": '/',
};
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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
};
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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
});
// 使用 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 的主要差異是什麼?
參考答案:
核心差異:
攻擊位置:
- Reflected XSS: 伺服器端 → 客戶端
- DOM-based XSS: 純客戶端
Payload 傳遞:
- Reflected XSS: 通過 HTTP 請求參數(
?param=value) - DOM-based XSS: 通過 URL Fragment(
#fragment)
- Reflected XSS: 通過 HTTP 請求參數(
伺服器可見性:
- Reflected XSS: 伺服器可以看到並記錄 Payload
- DOM-based XSS:
#後的內容不會發送到伺服器
防禦層:
- Reflected XSS: 可在伺服器端防禦
- DOM-based XSS: 只能在客戶端防禦
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() 的危險性:
- 執行任意代碼:
eval(userInput); // 用戶可以執行任何 JavaScript 代碼!
// 攻擊者輸入: "alert(document.cookie)"
// 結果: Cookie 被竊取
- 繞過所有防護:
// 即使你以為輸入是安全的...
var userInput = "2+2; alert(document.cookie)";
var result = eval(userInput); // 執行了 alert!
- 性能問題: 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
}額外建議:
- 使用 CSP 禁用 inline scripts
- 使用 Trusted Types
- 實施長度和格式驗證
- 記錄異常輸入
- 定期安全審計
9️⃣ 重點回顧
核心概念
DOM-based XSS 的獨特性:
- 攻擊完全發生在客戶端
- 使用 URL Fragment(
#)傳遞 Payload - 伺服器無法檢測和記錄
- WAF 無效
危險的組合:Source + Sink:
- Source:
location.hash,location.search,document.referrer - Sink:
innerHTML,eval(),setTimeout(),document.write()
- Source:
防禦策略:
- ✅ 使用安全的 API(
textContent而非innerHTML) - ✅ 驗證所有來自 Source 的輸入
- ✅ 避免
eval和類似函數 - ✅ 實施 CSP
- ✅ 使用 Trusted Types
- ✅ 使用安全的 API(
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 路徑
📖 延伸閱讀
🔗 系列導航
- 上一篇: 03-3 Stored XSS:持久型跨站腳本攻擊
- 下一篇: 03-5 XSS 攻擊實例與演練
- 返回目錄: Web Security 系列
📝 本文完成日期: 2025-01-15 🔖 標籤: #WebSecurity #XSS #DOMbasedXSS #JavaScript #Django #面試準備