首頁 / IT 趨勢洞察 / CSP 強化實戰
技術洞察 / 網站資安

企業網站 CSP 強化實戰:從 unsafe-inline 到 Mozilla Observatory A+

網站資安 · 2026 年 5 月 · 凱茂資訊技術團隊 · 閱讀時間 12 分鐘
分享: LINE 分享
快速回答:Content Security Policy(CSP)是瀏覽器層級的 XSS 防護機制,透過 HTTP header 告訴瀏覽器「哪些腳本來源可以執行」。移除 'unsafe-inline' 的關鍵在於分階段:先掛 Report-Only 收集違規 2 週、再抽 inline 事件處理改 event delegation、最後算 SHA-256 hash 切 enforce。凱茂自家網站從 baseline 走到 Mozilla Observatory A+ 用了 25 個 commit、24 小時,砍掉 358 個 inline event handler 跟 73% 的 inline style 屬性。

為什麼 CSP 是企業網站第一道、也是最便宜的 XSS 防線

網站被駭往往不是後端 SQL injection,而是某個第三方 JS 套件被供應鏈攻擊(如 polyfill.io 事件)、或某段使用者輸入沒過濾被注入 inline <script>。傳統做法是逐一過濾輸入、定期掃描,但 瀏覽器層級的 CSP 攔截等於多了一道防線:即使攻擊腳本確實被注入頁面,只要該腳本不在 CSP 白名單裡,瀏覽器拒絕執行。

CSP 的成本特別低:不買產品、不改後端架構,只需要在 HTTP response 加一條 header。但前提是網站不能用 'unsafe-inline' ——而幾乎所有沒做過 CSP 加固的網站都用了 'unsafe-inline',等於把這道防線開洞給整片開放。

一、為什麼移除 'unsafe-inline' 這麼難?

傳統網站把 onclickonloadonsubmit、JSON-LD、critical CSS 都直接寫在 HTML 裡。每段 inline 內容不一樣,瀏覽器無法用一條規則統一信任。CSP 規範給三條路:

  • Hash:對 inline body 算 SHA-256,把 hash 列進 script-src。每改一次內容,hash 就要重算。
  • Nonce:每次 response 動態生成 nonce,inline 標籤帶 nonce="..."。需要動態 backend 處理 response。
  • 抽出:把 inline 改成 <script src="...">,由 host 白名單覆蓋。

純靜態 HTML + IIS 部署無法用 nonce(要動態 response 寫入),所以只剩 hash 跟抽出兩條路。我們網站的工程量是這樣分布的:

  • 358 個 inline event handler(onclick / onload / onsubmit / onmouseover)→ 全部抽到 event delegation
  • 49 個 unique inline <style> 區塊 → SHA-256 hash 寫進 style-src
  • 285 個 <link rel="stylesheet" media="print" onload="this.media='all'"> → 改 data-async-css 由共用 .js 接管
  • 8,600 個 style="..." 屬性 → 抽到 BEM-style class,目前清到 2,286(-73%)
  • 667 段 inline <script> → 抽到 shared-events.js / petite-init.min.js / 算 hash

二、階段化導入路徑(6 個 Phase)

Phase 1:Report-Only baseline,不阻擋頁面

先掛 Content-Security-Policy-Report-Only 描述目前現況(含 'unsafe-inline''unsafe-eval'、所有第三方 CDN domain)。違規不會被阻擋,但會 POST 到 report-uri 收集。跑 2 週後檢查違規 log——任何違規都是「意料外行為」(攻擊嘗試、瀏覽器擴充注入、遺漏的 domain)。

我們網站跑 2 週只收到 3 個違規,全部是漏報的 GTM domain,補進白名單後即清空。這個階段的目的是確認沒有未知第三方——很多企業網站業務團隊偷加廣告 pixel / 行銷 iframe 沒通知 IT,CSP-RO 階段一次抓出來。

Phase 2-3:抽 inline 事件處理

onclickonloadonsubmit 改成 event delegation。在共用 shared-events.jsdocument.addEventListener('click', e => ...),按 data-trackdata-hide-persistdata-remove 等屬性分發。這樣 onclick attribute 從 1,030 個收到 0 個。

GA4 lazy loader(每頁頭部 inline <script> 算 100+ 行)也從 119 個檔案抽出統一 source。

Phase 4:onload race-safe + inline style hash

原本的 <link rel="stylesheet" media="print" onload="this.media='all'">(critical CSS 的 print-then-apply 技巧)改成 <link data-async-css>,由 shared-events.jsDOMContentLoaded 統一 swap。<noscript><link> 仍是 no-JS fallback。

剩餘 inline <style> 算 SHA-256 hash 寫進 style-src。49 個 unique hash 涵蓋全站。

Phase 5:切 enforce + 分目錄 web.config

把 header 從 Content-Security-Policy-Report-Only 換成 Content-Security-Policy,移除 script-src 'unsafe-inline'。這時候 hash 必須完全對應,否則該 inline 區塊整段被擋。

我們網站 inline 集合算出 315 個 unique hash,全部寫進 CSP value 是 17.7 KB,超過 Cloudflare 16 KB header 上限。解法是按目錄拆 bundle:

路徑web.confighashes大小
root + lp + solutions/web.config1116.7 KB
articles/*/articles/web.config17810.3 KB
case-studies/*/case-studies/web.config262.1 KB
admin/*(Express serve)server.js middleware51 KB

Phase 6:admin / 後台路徑獨立 enforce

後台是真正攻擊面(攻破即可改全站、看客戶資料),CSP 要單獨更嚴。我們的 admin 走 IIS rewrite 至 Express,CSP 由 server.js middleware 設置。但有一個雷:IIS web.config 的 customHeaders 會把 root CSP append 到 Express response,把 admin bucket CSP shadow 掉。修法是 web.config 加 <location path="admin"><customHeaders><remove name="Content-Security-Policy" /> 讓 Express 的 header pass through。

三、實作三大技術細節(踩坑紀錄)

坑 1:CRLF normalization

CSP hash 必須對 normalized body(HTML parser 會把 CRLF / CR 一律轉成 LF)算 sha256。Windows 開發環境的 .html file 多半是 CRLF,build script 必須先 body.replace(/\r\n?/g, '\n') 才算 hash,否則所有 hash 都對不上瀏覽器算的,整片頁面被擋。

這個我們花了一小時 debug 才發現——dev 看 OK 但 prod 全爆。

坑 2:Runtime-injected style

有些 JS 套件動態 document.createElement('style') + style.textContent = '...' + appendChild。這段 CSS 不在任何 .html source 裡,build script 掃不到。我們的 chat-widget 套件就是這樣——lazy load 後注入 50 行 CSS。

修法是 build script 直接 parse minified JS 字串字面量,把 textContent 那段算 hash 加進所有 bucket:

const src = fs.readFileSync('js/chat-widget.min.js', 'utf8');
const m = src.match(/\.textContent\s*=\s*("(?:\\.|[^"\\])*")/);
const css = JSON.parse(m[1]);
const hash = crypto.createHash('sha256').update(css, 'utf8').digest('base64');

坑 3:Pre-commit 自動化

CSP enforce 上線後,每次改 .html 都要重算 hash 並更新 web.config——手動跑非常容易忘記,忘記就是該頁上線壞掉。我們在 scripts/githooks/pre-commit 做:

  • 偵測 staged *.htmljs/chat-widget.min.js
  • 自動跑 node scripts/build-csp-config.js
  • 把更新後的 4 個 config 檔 git add 進此 commit
  • build 失敗就 abort commit

git config core.hooksPath scripts/githooks 讓 hook 跟 repo 同步,不只是個人 .git/hooks/。換機後跑 bash scripts/setup-githooks.sh 即可。

四、最終 CSP value(公開頁)

default-src 'self';
script-src 'self' 'unsafe-eval' <CDN whitelist> <sha256-... × 111>;
style-src 'self' <sha256-... × 44>;
style-src-attr 'unsafe-inline';   ← 暫保(8,600 個 style="..." 重構長期)
font-src 'self' data:;
img-src 'self' data: https: blob:;
connect-src 'self' <CDN whitelist>;
frame-src https://www.google.com https://www.googletagmanager.com;
object-src 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
report-uri /api/csp-report

唯一保留的 'unsafe-eval' 是 PetiteVue(公開頁)跟 Vue 3(admin)的框架限制——兩者都用 new Function() 編譯模板,不能移除。但這個是 需先有 XSS 才能利用的攻擊路徑,inline 防線清乾淨後實質風險已大幅降低。

五、Mozilla Observatory 評分結果

動工前的 baseline 約 D 級(CSP 充滿 unsafe-inline,多項 header 沒掛)。動完後跑 Mozilla Observatory v2:

項目結果分數
redirection✓ pass0
referrer-policy✓ pass+5
strict-transport-security✓ pass0
subresource-integrity✓ pass+5
x-content-type-options✓ pass0
x-frame-options✓ pass+5
cross-origin-resource-sharing✓ pass0
cookies✓ pass0
content-security-policyfail('unsafe-eval')-10

總分 105 / 100,Grade A+。唯一失分是 'unsafe-eval',框架限制不可移除。

六、給其他企業 IT 主管的 5 個 takeaway

重點摘要

  • 先 Report-Only 跑 2 週:成本零,但會抓出業務團隊偷加的第三方 pixel、瀏覽器 extension、遺漏 domain。
  • Cloudflare 16 KB header 限制:純靜態網站全站 hash 通常超 16 KB,按目錄分桶寫多個 web.config 是務實解。
  • CRLF / runtime-injected style 是必踩的坑:dev 看 OK 但 prod 整片爆,先在 build script 內 normalize、再驗證 prod。
  • Pre-commit hook 是維運必備:CSP 上線後每改 HTML 就要重算 hash,沒自動化就是定時炸彈。
  • 'unsafe-eval' 通常移不掉:Vue / React / Angular 等框架都用 new Function(),能移除的代價是換框架,跟收益不成比例。Mozilla A+ 最多扣 -10 仍可達標。

需要協助為自家網站做 CSP 強化、或想瞭解資安標頭如何整合進 ISO 27001 / 銀行 RFP 應對?

預約 30 分鐘免費資安架構諮詢 →

相關方案:資安與網路方案

凱茂資訊為您提供完整的規劃、建置與維運服務,歡迎諮詢。

瞭解我們的資安防護方案 → 預約諮詢
IT 技術電子報

覺得這篇文章有幫助?

訂閱電子報,每月收到最新 IT 趨勢與實務文章

專業顧問諮詢

讀完這篇文章,是否有更多問題?

凱茂資訊提供 30 分鐘免費架構評估,由專業顧問針對您的企業現況給出具體建議,不推銷、不強迫。

預約 30 分鐘免費諮詢 預約諮詢

✓ 免費諮詢,無義務購買 ✓ 中部地區可現場拜訪 ✓ 一般於 1 個工作天內回覆

凱茂
安裝凱茂資訊 App
快速存取報修、報價與 IT 資源
需要 IT 顧問協助?
30 分鐘免費評估 · 一般 1 個工作天內回覆