2.3 點擊劫持、開放重導向與原型污染#
三個「不起眼」但真實的攻擊#
本章收錄三種常被忽略、卻在實務中大量出現的攻擊 —— 共同點:都利用了瀏覽器或框架的默認行為。
攻擊一:Clickjacking(點擊劫持,2008)#
原理:視覺欺騙#
<!-- evil.com -->
<style>
iframe { position: absolute; opacity: 0; top: 0; left: 0; width: 500px; height: 300px; }
button { position: absolute; top: 120px; left: 180px; }
</style>
<iframe src="https://bank.com/settings/delete-account"></iframe>
<button>抽獎!點我</button> <!-- 疊在透明 iframe 的「確認刪除」上 -->
使用者以為點「抽獎」,實際點了透明 iframe 裡的「刪除帳號」—— 視覺與實際操作目標錯位。
防禦一:CSP frame-ancestors(現代正解)#
Content-Security-Policy: frame-ancestors 'none'
告訴瀏覽器:本頁不允許被任何頁面內嵌。這是 CSP(2.4)框架指令的直接應用。
防禦二:X-Frame-Options(舊相容)#
X-Frame-Options: DENY # 全禁
X-Frame-Options: SAMEORIGIN # 只允同源內嵌
2009 年誕生、2013 年被 CSP 取代但為相容性仍應同時發送 —— 縱深防禦的「多發一份標頭成本為零」。
攻擊二:Open Redirect(開放重導向)#
原理:重導向目標可控#
// 登入後跳回來源頁 —— 標準功能
res.redirect(req.query.returnTo); // ❌ 未驗證!
https://example.com/login?returnTo=https://evil.com/phish
攻擊者利用你的域名包裝釣魚連結:使用者在信箱看到 example.com 開頭的網址,登入後卻被送去假頁 —— 信任被借用(1.2 Spoofing)。
防禦:白名單驗證#
const ALLOWED = ['/', '/todos', '/settings'];
function safeReturnTo(r) {
if (!r) return '/';
try {
const u = new URL(r, 'https://example.com');
return u.origin === 'https://example.com' && ALLOWED.includes(u.pathname)
? u.pathname : '/';
} catch { return '/'; }
}
- 只允許同源路徑(絕對 URL 一律拒絕或解析後驗域名)
- 白名單原則再次勝出(2.1 同理)
攻擊三:原型污染(Prototype Pollution,2018 命名)#
根源:JS 原型鏈的全域性#
JS 物件查詢屬性時沿原型鏈向上找:
const obj = { name: 'x' };
obj.toString; // 自己沒有 → 沿鏈找到 Object.prototype.toString
若攻擊者能把屬性寫到 Object.prototype,所有物件同時被污染:
// 經典漏洞模式:深度合併(merge)時把 __proto__ 當普通鍵
function deepMerge(target, src) {
for (const k in src) {
if (typeof src[k] === 'object') deepMerge(target[k] ??= {}, src[k]);
else target[k] = src[k];
}
}
deepMerge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
({}.isAdmin) // true —— 全域生效!
危害鏈#
flowchart LR
A[污染 Object.prototype] --> B[繞過判斷:\nif (cfg.admin)]
A --> C[引發 XSS:\n污染 innerHTML 相關屬性]
A --> D[RCE 前奏:\n污染 Node.js 的 env/選項物件]實際案例:2019 年起多個熱門 npm 套件(lodash 的舊版 merge 等)被證實可原型污染;在 Node 服務中可升級為 RCE(污染 child_process 選項)。
防禦#
// 1. 合併時過濾危險鍵
const SAFE = (k) => !['__proto__', 'constructor', 'prototype'].includes(k);
// 2. 建立無原型物件做容器
const target = Object.create(null); // 沒有原型鏈可污染
// 3. 凍結原型(防禦性加固)
Object.freeze(Object.prototype);
// 4. 深凍結與結構化驗證(zod schema 只接受已宣告欄位)
zod(前書 4.4)的 schema 驗證天然防原型污染:未知鍵被丟棄,__proto__ 根本進不了物件。
通用教訓:預設行為即攻擊面#
| 預設行為 | 被利用 | 防禦 |
|---|---|---|
| 頁面可被 iframe | Clickjacking | frame-ancestors |
| redirect 參數自由 | 開放重導向 | 白名單 |
| 原型鏈可寫 | 原型污染 | 過濾鍵 + schema |
| GET 可觸發動作 | (2.2 CSRF) | 語意紀律 |
列舉系統的每個「自動」行為,問一句「誰可以觸發它」 —— 這是 1.2 攻擊面盤點的微觀執行。
習題#
- 為你的站設定 frame-ancestors,並用
curl -I驗證兩個標頭都存在。 - 寫一個安全的 returnTo 驗證,並測試
//evil.com、https://example.com.evil.com、/\evil.com等繞過變體。 - 為什麼
Object.create(null)能免疫原型污染?它的代價(失去哪些方法)?