主題: Web platform
CSP 不要先封鎖:用 Report-Only 找出真實依賴,再逐步收緊
CSP 的 Report-Only 階段不是暫停保護,而是用違規報告辨認真正依賴、雜訊與舊寫法;之後再以 nonce、hash 與最小權限安全上線。
第一次替網站加 Content Security Policy(CSP),最危險的動作通常不是「少放一個來源」,而是看見一串違規報告後,把所有網域全塞進 allowlist。頁面暫時不壞了,但 policy 也可能寬到幾乎沒有防護效果。
CSP 的角色是限制頁面可以載入與執行什麼,替輸出編碼與 sanitization 多加一層防線;它不是取代 XSS 修補的萬靈丹。要讓這一層真的有用,先把 Report-Only 當成觀測期,而不是一次性的相容性開關。
Report-Only 不會擋住請求,但會讓草稿 policy 留下足跡
Content-Security-Policy-Report-Only 是 HTTP 回應標頭。瀏覽器會依草稿 policy 回報本來會被擋下的資源,但不會實際封鎖它;因此可以先看見正式環境的依賴,再決定怎麼改。它不能寫在 <meta> 裡。
一個抽象的起點如下:
Reporting-Endpoints: csp="https://reports.example.com/csp"
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'nonce-{random}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
report-to csp;
report-uri /csp-report;
這不是可直接複製的正式設定:{random} 必須在每個回應重新產生,而 endpoint、瀏覽器支援範圍與實際資源都要依系統調整。重點是 report-to 要對應 Reporting-Endpoints 中的名稱;考慮跨瀏覽器相容性時,MDN 仍建議一併送出較舊的 report-uri。
動態迷因(展開/收合)
Report-Only 的價值不只是避免破壞。它迫使我們回答:這個 script、style、image 或 frame 是哪個功能載入的?我們擁有它嗎?它是否仍需要?同一個網域可能是必要的付款元件,也可能只是舊標籤、實驗遺留物,或不該為全站開放的第三方載入器。
不要把報告直接變成 allowlist
建議把每一類違規分成三條路徑處理:
- 自己的程式:修正 bundle、模板或內嵌寫法,讓它符合預期 policy。
- 確實需要的外部服務:確認供應商、用途與載入鏈,再只開放必要的 directive 與來源。
- 沒有明確擁有者的項目:先查請求發生的頁面與環境;沒有足夠證據就不要批准。
這個分類比「報告數量是否下降」更重要。CSP allowlist 容易隨第三方腳本膨脹;MDN 特別提醒,過寬的來源清單可能容許不安全網域,最後抵消 CSP 的目的。Buttondown 的實際導入經驗也很貼切:先用 Report-Only 收集違規,再判斷該移除載入,還是真的允許來源,而不是直接開啟封鎖模式賭一次。
收緊 script-src,先處理程式碼形狀
真正有效的 CSP 不只是在 script-src 後面列出更多網域。strict CSP 以可辨認的程式碼為起點:
- nonce 適合動態回應。伺服器每次回應都產生不可預測的新值,並讓 header 和允許執行的
<script>共用它。 - hash 適合內容固定的 inline script。雜湊會對準那段精確內容,連空白改動都可能讓它失效。
strict-dynamic讓已由 nonce 或 hash 信任的起點腳本,能在支援的瀏覽器中載入後續腳本;它不是把任意 script 都設為可信。
因此,看到 onclick="…"、字串形式的 setTimeout(),或 eval() 時,不要先以 unsafe-inline 或 unsafe-eval 把門打開。前者會允許廣泛的 inline script,後者允許把字串當作 JavaScript 執行。把 event handler 改成 addEventListener()、把程式資料保留為資料,通常是更好的長期修正。
動態迷因(展開/收合)
必要時可以先用較小的例外,例如對特定 inline event handler 使用 unsafe-hashes;但這仍是權衡,不是預設答案。每一個例外都應寫下擁有者、原因與移除條件。否則 policy 很容易從一條安全規則,慢慢長成無人敢碰的歷史清單。
一個可實行的發布順序
我會把 CSP rollout 拆成幾個可退回的步驟:
- 先寫出目標 policy,包含
object-src 'none'與base-uri 'none'等明確邊界。 - 以回應標頭在真實流量中啟用 Report-Only,並收集可追溯的報告。
- 逐項修正自己的程式、審查外部服務,拒絕無法解釋的來源。
- 在代表性頁面與使用流程驗證後,用
Content-Security-Policy啟用同一份已收斂的 policy。 - 保留後續觀測,讓下一次第三方或模板變動不會悄悄把邊界打穿。
第 4 步才是「封鎖」;前面三步是在避免把正常使用者當作測試工具。CSP 最好的樣子不是一長串網域,而是一份能說明每個可執行來源為何存在、誰負責、何時該移除的執行契約。
我學到什麼
- 我會先以 Report-Only 看見真實依賴,再讓 enforcing policy 影響使用者;觀測結果是審查輸入,不是自動 allowlist。
- nonce、hash 與
strict-dynamic的重點是建立可追溯的信任鏈,而不是把來源清單越列越長。 unsafe-inline與unsafe-eval指向的是程式碼結構問題;能重構時,重構通常比永久例外更安全。- CSP 是 XSS 防護的另一層,不會取代輸出編碼、sanitization 與一般的安全修補。