CSP(內容安全政策)
簡介
在現代 Web 應用中,跨站腳本(XSS) 是最常見且危害最大的攻擊手法之一。攻擊者往往藉由注入惡意 JavaScript,竊取使用者的認證資訊、操控頁面行為,甚至發動釣魚攻擊。傳統的防禦方式(如輸入過濾或轉義)雖然必要,但往往難以保證 100% 完整,且維護成本高。
內容安全政策(Content Security Policy,簡稱 CSP) 是瀏覽器層面的防護機制,它允許網站管理者以白名單的方式明確規範哪些資源可以被載入、執行。只要瀏覽器遵守 CSP,任何未被授權的腳本、樣式、圖片或框架都會被自動阻擋,從根本上降低 XSS 與資料外洩的風險。
本篇文章將從概念、指令、實作範例、常見陷阱與最佳實踐,帶你一步步掌握 CSP,讓你的前端專案在安全性上更上一層樓。
核心概念
1. CSP 的運作原理
CSP 透過 HTTP 回應標頭 Content‑Security‑Policy(或 <meta http‑equiv="Content‑Security‑Policy">)向瀏覽器宣告「允許的資源來源」。瀏覽器在解析 HTML、CSS、JavaScript 時,都會比對這份白名單;一旦發現違規的載入行為,就會拋出 CSP violation,並根據設定決定是否阻止該資源。
重點:CSP 不是「防止」資源被寫入,而是「限制」資源的 來源 與 執行方式。
2. 常見指令(Directives)
| 指令 | 說明 | 常見值 |
|---|---|---|
default-src |
所有未明確指定的資源類型的預設來源 | 'self'、https: |
script-src |
JavaScript 來源 | 'self'、https://cdn.example.com、'nonce-xxxx'、'sha256-xxxx' |
style-src |
CSS 來源 | 'self'、'unsafe-inline'(不建議) |
img-src |
圖片來源 | 'self'、data:、https://images.example.com |
connect-src |
XHR、WebSocket、EventSource 等連線來源 | 'self'、https://api.example.com |
font-src |
字型檔來源 | 'self'、https://fonts.gstatic.com |
frame-src |
<iframe>、<frame> 的來源 |
https://www.youtube.com |
object-src |
<object>、<embed>、<applet> 的來源 |
none(建議禁用) |
base-uri |
<base> 標籤的允許來源 |
'self' |
report-uri / report-to |
CSP 違規報告的接收端點 | https://csp-report.example.com/ |
小技巧:如果你只想針對某一類資源作限制,其他資源就使用
default-src作為後備。
3. nonce 與 hash:允許內嵌腳本的安全方式
3.1 為什麼不直接使用 unsafe-inline?
unsafe-inline 會允許所有內嵌腳本(<script>、onclick 等)執行,等於把 CSP 的防護關掉。在正式環境絕不要使用。
3.2 使用 nonce(一次性隨機字串)
伺服器在每次回應時產生一個隨機字串(如 nonce-abc123),在 CSP 標頭裡宣告,並把相同的 nonce 加到允許執行的 <script> 標籤上。只有帶有正確 nonce 的腳本會被執行。
// 伺服器端 (Node.js + Express)
app.use((req, res, next) => {
const crypto = require('crypto');
const nonce = crypto.randomBytes(16).toString('base64'); // 產生 22 字元的 base64
// 把 nonce 存到 res.locals,稍後渲染模板時可使用
res.locals.cspNonce = nonce;
// 設定 CSP 標頭
res.setHeader(
'Content-Security-Policy',
`script-src 'self' 'nonce-${nonce}'; object-src 'none'; base-uri 'self'`
);
next();
});
<!-- HTML 模板 (ejs) -->
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>使用 nonce 的 CSP 範例</title>
</head>
<body>
<h1>Hello CSP</h1>
<!-- 只有帶有正確 nonce 的腳本會被執行 -->
<script nonce="<%= cspNonce %>">
console.log('這段腳本被允許執行');
</script>
</body>
</html>
3.3 使用 hash(內容雜湊)
如果腳本內容是固定不變的,可以直接把腳本的 SHA256(或 SHA384、SHA512)雜湊寫入 CSP。瀏覽器會比對腳本內容的雜湊值,只有匹配的腳本會被允許。
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>使用 hash 的 CSP 範例</title>
<!-- 計算後的 SHA256 雜湊值 -->
<meta http-equiv="Content-Security-Policy"
content="script-src 'self' 'sha256-3vJrV+5t1YQ0kB1V4KZ0Xc5K2XkKZx6X5p9wX8Q7vZc=';">
</head>
<body>
<script>
// 這段腳本的內容必須與上面的 hash 完全相同
console.log('hash 方式允許的腳本');
</script>
</body>
</html>
備註:若腳本內容稍有變動(例如空白或註解),hash 就會失效,需要重新計算。
4. 報告機制:report-uri vs report-to
當 CSP 阻擋了資源或偵測到違規時,瀏覽器會將違規資訊 POST 給你指定的端點。report-uri 是較早的規範,report-to 則是新一代、支援更豐富的報告格式。
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net;
report-to csp-endpoint
Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://csp-report.example.com/collect"}],"include_subdomains":true}
在伺服器端,你可以簡單寫一個接收報告的 API,收集違規資訊作為安全審計的依據。
// Express 接收 CSP 報告
app.post('/csp-report', express.json({ type: ['application/csp-report', 'application/json'] }), (req, res) => {
console.log('收到 CSP 報告:', req.body);
// TODO: 儲存至資料庫或發送告警
res.status(204).end(); // 回應空白表示已收到
});
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
使用 unsafe-inline |
允許任意內嵌腳本,等同關閉 CSP。 | 改用 nonce 或 hash,或將腳本抽離至外部檔案。 |
忘記 script-src 包含 'self' |
若只寫了 CDN,自己網站的腳本會被阻擋。 | 在 script-src 中同時加入 'self'。 |
style-src 漏寫 unsafe-inline |
許多 UI 框架使用內嵌樣式,若未允許會導致樣式失效。 | 使用 nonce/hash 允許必要的內嵌樣式,或改為外部 CSS。 |
| 報告端點未正確設定 | CSP 違規不會被記錄,失去偵測機會。 | 同時設定 report-uri(舊版)與 report-to(新版),確保端點可接受 application/csp-report。 |
| CSP 與第三方服務衝突 | CDN、分析工具、廣告等常有額外資源需求。 | 逐一列出所需來源,或使用子域名隔離第三方腳本。 |
忘記更新 hash |
稍微改動腳本就會導致 CSP 錯誤。 | 若腳本頻繁變動,改用 nonce;若固定,寫自動化腳本產生 hash。 |
最佳實踐清單
- 從
default-src開始:先設default-src 'self',確保所有未明確允許的資源都被封鎖。 - 逐步放寬:根據實際需求,僅在必要的指令上加入額外來源(如
script-src https://cdn.jsdelivr.net)。 - 使用
nonce或hash:盡量避免unsafe-inline。 - 啟用報告:在開發環境先使用
report-only(Content-Security-Policy-Report-Only)觀察違規,再正式上線改為強制模式。 - 自動化測試:將 CSP 產生與驗證納入 CI pipeline,確保每次部署不會破壞政策。
- 結合 Helmet(Node.js)或 Django‑csp 等套件:減少手動寫標頭的錯誤機會。
// 使用 Helmet 自動產生 CSP(Node.js 範例)
const helmet = require('helmet');
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdn.jsdelivr.net", (req, res) => `'nonce-${res.locals.cspNonce}'`],
styleSrc: ["'self'", "'unsafe-inline'"], // 若必須允許內嵌樣式
imgSrc: ["'self'", "data:", "https://images.example.com"],
reportUri: '/csp-report',
},
})
);
實際應用場景
1. 電子商務平台
在購物車、結帳流程中,若被注入惡意腳本,攻擊者可以竊取信用卡資訊或改寫付款金額。透過 CSP,僅允許自家域名與受信任的支付金流 CDN,所有第三方腳本(如廣告)若未列入白名單即被阻擋,大幅降低風險。
2. 企業內部管理系統(Intranet)
企業系統常使用大量的 AJAX 與 WebSocket 互動。使用 connect-src 明確限制只允許公司內部 API(https://api.company.com)與特定的即時通訊服務,防止惡意外部站點利用 XSS 發起跨站請求(CSRF + XSS)。
3. 單頁應用(SPA)
React、Vue、Angular 等框架會在客戶端大量產生內嵌腳本(如動態生成的 style 標籤)。此時可在建置流程中自動為所有產生的腳本加上 nonce,或使用框架提供的 CSP 插件(如 react-helmet)統一管理。
// React 使用 react-helmet 設定 CSP
import { Helmet } from 'react-helmet';
function App() {
const nonce = crypto.randomUUID(); // 假設在服務端產生並注入
return (
<>
<Helmet>
<meta
httpEquiv="Content-Security-Policy"
content={`default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'unsafe-inline'`}
/>
</Helmet>
<h1>SPA with CSP</h1>
<script nonce={nonce}>console.log('SPA script');</script>
</>
);
}
4. 多租戶 SaaS 平台
每個租戶可能會自行上傳自訂的 HTML 小工具。平台可以為每個租戶產生獨立的 nonce,只允許該租戶的腳本執行,避免租戶之間的腳本互相干擾或攻擊。
總結
- CSP 是瀏覽器層面的白名單機制,能有效阻止未授權的腳本、樣式與其他資源執行。
- 透過
default-src、script-src、style-src等指令,配合nonce、hash取代危險的unsafe-inline,即可在不犧牲功能的前提下提升安全性。 - 報告機制(
report-uri/report-to)讓開發者在上線前先以 Report‑Only 模式觀測違規,確保政策不會誤殺正當資源。 - 常見的陷阱包括忘記加入
'self'、濫用unsafe-inline、以及未正確設定報告端點;遵循 「最小權限」 與 「逐步放寬」 的原則,可大幅降低錯誤配置的風險。 - 在實務上,從 電商、內部系統、SPA 到多租戶平台,CSP 都是提升前端安全的必備工具;搭配自動化建置、Helmet、react‑helmet 等套件,可讓政策的維護變得輕鬆且一致。
透過本文的概念說明與實作範例,你已掌握在 JavaScript 專案中導入 CSP 的全流程。立即在開發環境測試、調整政策,然後在生產環境啟用,讓你的網站在面對 XSS 攻擊時,擁有堅實的防護屏障。祝開發順利,安全無憂!