2026-06-01 早上,甲方把一份 ZAP 弱掃報告丟過來,四條中風險全是 CSP(Content Security Policy,瀏覽器用來限制頁面能載入哪些來源的一層 HTTP header 防護)相關,附帶一句「中風險都要修」。最難纏的一條是 style-src 'unsafe-inline'

我把報告丟給 Claude Code,請它評估。那天它給的結論是:這條技術上修不乾淨,只能寫一封信去說服甲方接受。兩天後,我們把它從正式機完全拿掉了,全站 94 個頁面實測零影響。

中間發生的事,比結論本身有意思——因為它一開始錯得很徹底,而我差點就照單全收了。

先搞清楚 V3 到底還剩幾條

Claude Code 先把我們 Vue 3 專案正式機的 Web.config 拉出來對。弱掃報告裡那串髒兮兮的 CSP 其實是同台 server 上舊版 AngularJS 站台的,不是 V3 的。V3 的狀況比我以為的好:

1
2
3
4
script-src 'self' 'wasm-unsafe-eval'
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com
img-src 'self' data: blob: https://js.arcgis.com https://wmts.nlsc.gov.tw ...
connect-src 'self' https://wragis.e-land.gov.tw https://js.arcgis.com ...

它指出:script-src 已經乾淨——沒有 unsafe-inline、沒有 unsafe-eval,只有 wasm-unsafe-eval(這是給 WebAssembly 用的,跟能執行任意字串的 eval 是兩回事)。img-srcconnect-src 也早就是明確白名單,不是 https: 那種等於開大門的萬用字元。

四條中風險,V3 自動消掉三條。剩下唯一一條,就是 style-src 'unsafe-inline'

第一階段:它認定這是 Vue 的結構性依賴

Claude Code 的判斷很直覺,我聽起來也合理:Vue 的 :style binding 會在 DOM 上產生 style="..." 屬性,只要專案裡有任何一個 :style,CSP 的 style-src 就不能拿掉 unsafe-inline,不然樣式會被擋。

它去盤點專案裡的 xgis 框架元件,數出 24 個 :style,照能不能 class 化分了難度級別:

  • 簡單的(顏色、縮排)→ 改成 CSS class 或 data- 屬性
  • 硬的(datatablecol.width 來自資料庫的任意像素值)→ 沒辦法枚舉成 class
  • 近乎無解的(ModalPopup 拖曳時的 x/y 座標,是即時滑鼠位置)→ 它寫下「無解」兩個字

結論呼之欲出:純 class 完全移除,理論上不可能,拖曳座標那關過不去。

於是它幫我擬了一封給甲方的「缺失改善說明」,論述核心是:style-srcunsafe-inline 因 CSP 規格限制無法移除——nonce(每次請求產生的一次性隨機 token,用來放行特定 inline 內容)這個機制只能套用在 <style> 元素上,套不到元素的 style="" 屬性,而前端執行期的動態樣式必然產生這種屬性。

這封信寫得不錯。它還特別提醒我一個寫作判斷:不能寫「實務上不可能拿掉」,會被懂行的資安顧問一句「那你上 nonce 啊」打回來,所以改成「規格限制」這個防守得住的版本。我看了挺滿意,也信了。

問題是,整封信建立在一個錯誤的前提上。

轉折:我一句追問,把它問醒了

我補了一個問題:「v-show 是不是也會被擋?」

v-show 是靠切換 display: none 來顯示/隱藏元素的指令。Claude Code 回得很快,大意是:會,它產生的就是行內樣式。然後它 grep 了一下,給了一個斬釘截鐵的數字——57 個。接著一句話我印象很深:

style-src unsafe-inline 在 Vue 3 裡是結構性依賴,不是選擇性的。React、Angular 也一樣。

那語氣篤定到我差點就收下了。57 個 v-show 全部要改成 v-if,行為還不一樣(v-if 會銷毀 DOM,保留不了 form 已填的值),這條路看起來死定了。

但有個地方卡住我。如果三大框架全部都不能拿掉 style-src unsafe-inline,那不就等於全世界的 SPA 都被迫開著這個洞?我不太相信,就回了一句:

React、Angular 也一樣?三大框架全死嗎?那豈不是只有原生能做到?

這句話讓它停了下來。

它回去翻 CSP 規格,然後改口了,大意是:剛才講得太武斷,要更正。style-src 其實只管兩件事:

  • HTML response 裡靜態的 style="..." 屬性
  • <style>...</style> 元素

它不管 JavaScript 在執行期透過 CSSOM(CSS Object Model,瀏覽器把樣式開放給 JS 操作的介面)改的樣式,也就是 element.style.display = 'none' 這種寫法。

而 Vue 的 v-show 底層就是 el.style.display = 'none':style binding 底層是 el.style.setProperty(...)——全部是 CSSOM 操作,不是 setAttribute('style', ...)。CSP 的 style-src 根本碰不到它們。連它先前判定「無解」的 ModalPopup 拖曳座標也一樣,走的是 el.style.setProperty('--modal-x', x + 'px'),CSSOM,不受管。

它最後補了一句:「我剛說 57 個全死是錯的,對不起。」

三大框架真實的狀況是這樣:

框架 動態樣式寫法 需要 style-src unsafe-inline?
React style={{ }} 不需要(CSSOM 操作)
Angular [ngStyle][style.x] 不需要(CSSOM 操作)
Vue 3 :stylev-show 不需要(CSSOM 操作)

那真兇是誰?Claude Code 推論:執行期動態注入 <style> 元素的東西——<style> 元素受 style-src 管。在這個專案裡最大嫌疑只有一個,地圖元件的渲染引擎 ArcGIS JavaScript API。它說,把 unsafe-inline 拿掉、開地圖頁看 console 噴什麼,幾乎肯定是 ArcGIS。

在動手拔之前,得先認一筆帳。第一階段它為了「減少 :style 用量」改的那三個元件——LoadingOverlayFileUploadTreeView——從 CSP 角度來看是白改的。它們的 :style 本來就走 CSSOM,本來就不會觸發任何 violation。功能沒改壞,但動機是錯的。在搞清楚一個機制到底管什麼之前就動手,改的全是無用功——這筆帳它跟我都有份。

第二階段:直接拔掉測,結果又踩一個坑

我的反應很乾脆:「拿掉,直接測看看就知道了。」

Claude Code 把 Web.configunsafe-inline 拿掉,然後犯了第二個錯——它去改 vite.config.js 裡那份 dev server 的 CSP,想用 Playwright 在開發環境跑一輪頁面巡覽。

結果一團糟。截圖裡整個版面垮成裸 HTML,Bootstrap 樣式全失效,登入表單的按鈕都點不準。Playwright 收集到 81 個 CSP violation,全部來自一個來源:/@vite/client

它盯著那 81 個假陽性想了一下才通:dev 模式下,vite 是用「動態注入 <style> 元素」的方式把所有 CSS(含 Bootstrap、scoped style)塞進頁面的。一拿掉 unsafe-inline,這些 <style> 全被 CSP 擋掉,整站 CSS 當然就垮了。但這是 vite 開發伺服器的特性,正式機 build 之後 CSS 是打包成外部 .css 檔、用 <link> 載入,完全沒這問題。

換句話說:dev 環境根本沒辦法驗證正式機的 CSP。那 81 個 violation 一個都不算數。

(這個坑其實它之前就該記得——dev 正常、build 壞掉的「本機無法重現」類陷阱,這專案踩過不只一次。)

它把 vite.config.js 改回去(dev 必須保留 unsafe-inline,否則開發時版面會垮),換成正確的驗證環境:用 npm run build 的產物,掛在本機 IIS(localhost:56961,實體路徑指向專案根目錄,套用改過的真 Web.config,而且同源 API 能登入)。這套環境跑出來的任何 violation,才是正式機真的會發生的。

結局:94 個頁面,真相一次說清楚

換對環境之後,第一次 IIS 實測就乾淨得漂亮。

登入成功,落在 /sys/user。GIS 地圖頁完整載入,ArcGIS 的 500 多個 esri/calcite 元件全到位。整輪只有 3 個 violation,而且全部來自 ArcGIS 內部:

  1. script-src evaltypedArrayUtil.js
  2. style-src-attrGIS.js(ArcGIS 設定 inline style 屬性)
  3. style-src-attrManagedCanvas.js(canvas 渲染)

這三個其實分屬兩條 directive。第一個是 script-srceval,跟我要拔的 style-src 無關——ArcGIS 拿 eval 做型別陣列的最佳化探測,被擋掉後就退回純 JS 的 TypedArray 路徑,功能等價,只是少了一點最佳化。後兩個才是 style-src-attr(CSP Level 3 從 style-src 細分出來、專管 style="" 屬性的子指令),被擋的是 ArcGIS 直接設在元素上的 inline 屬性,但它的視覺主要靠自己打包的外部 CSS,這幾個屬性擋掉不影響呈現。

0 個來自 Vue app。它先前的推論被實測證實了。

接著我說「除了 GIS,其他也檢查檢查」,它寫了支全站稽核腳本,登入後逐頁巡覽 93 個頁面——統計圖表、LayerEditor、所有模組的 CRUD 列表,還特地對 5 個頁面開 modal(測那個它曾判定「無解」的拖曳定位)。

1
2
3
4
巡覽頁數: 93(含統計圖表、LayerEditor、所有 CRUD、5 頁開 modal)
CSP violation 總數: 0
未捕捉 JS 例外: 0
其他 console error: 0

加上 GIS 主地圖頁,總共覆蓋 94 個頁面/功能。除了 ArcGIS 那 3 個(地圖底圖、圖層、marker、widget UI 全部正常,零 JS 例外)之外,整個 Vue 應用零 violation。

style-src 'unsafe-inline',真的拿掉了。

帶得走的幾個技術要點

如果你也在跟弱掃報告裡的 style-src unsafe-inline 纏鬥,這次排查留下幾條可以直接用的結論:

第一,搞清楚 style-src 的邊界。 它只管 HTML 裡的 style="" 屬性和 <style> 元素,不管 JS 透過 CSSOM(element.style.xxx)改的樣式。

樣式來源 受 style-src 管?
HTML 裡寫死的 <div style="color:red"> 是(需要 unsafe-inline 或 hash)
<style> 元素(含 JS 動態 appendChild 注入的)
element.style.color = 'red'(CSSOM)
Vue :style / v-show、React style={{}}、Angular [ngStyle] 否(編譯後都是 CSSOM)

第二,真正逼你保留 unsafe-inline 的,通常不是你的框架,而是某個在執行期注入 <style> 元素的第三方 library<style> 元素受 style-src 管)。先去找那個 library,別再改自己的 :style。我們這裡是 ArcGIS——它 4.x 的 Calcite 元件用 Shadow DOM 封裝樣式(那部分不受 CSP 管),會噴的只剩它直接設 inline 屬性的少數地方。這裡有個容易走錯的岔路:如果這個 library 注入的是內容每次都變的動態 <style>,用 hash(把 <style> 內容的 SHA256 列進白名單)也救不了——hash 只對內容固定的 <style> 有效。

第三,CSP 一定要在 build 產物 + 真實 server 上驗證。 開發伺服器(vite、webpack-dev-server 之類)為了 HMR 會用注入 <style> 的方式載 CSS,在這種環境測 style-src 只會拿到一堆 dev-only 的假陽性。我們在 dev 拿到 81 個 violation,在 IIS build 只剩 3 個——差距全是工具雜訊。

如果你要的是 CSP 的通論配置與分階段導入,我之前寫過一篇 Content Security Policy 安全性與實用性平衡指南,那篇談的是怎麼從零配起;這篇談的是一條具體的中風險怎麼從「以為修不了」變成「拿掉了」。

附:可以直接複製的弱掃改善說明範本

故事的結局讓那封信整個升級了。原本準備的是「風險接受」版(請甲方接受這條無法移除),實測之後可以直接改寫成「已移除」版——對審查單位來說,這是最有力的回覆,因為你不是請他們網開一面,而是真的把洞補了。

下面這份可以直接填:

缺失項目: Content-Security-Policy style-src'unsafe-inline'

處置方式:已移除

本系統正式機 Web.config 之 CSP 設定,已將 style-src'unsafe-inline' 移除,現行政策為 style-src 'self' https://fonts.googleapis.com

相容性驗證:
移除後經自動化工具(Playwright)對全站 94 個頁面/功能進行 securitypolicyviolation 事件稽核,涵蓋所有模組之列表、表單、統計圖表、彈出視窗及地圖頁。結果:前端應用程式碼產生之 CSP 違規數為 0、未捕捉 JavaScript 例外為 0,各功能版面與互動正常。

說明:前端框架(Vue 3)之動態樣式(:stylev-show 等)於執行期均以 CSSOM 介面操作(element.style),不產生受 style-src 管制之 style="" 屬性或 <style> 元素,故移除 'unsafe-inline' 不影響前端功能。

殘餘事項說明(地圖元件):
第三方地圖函式庫(ArcGIS JavaScript API 4.x)於地圖頁面執行期,會動態設定少數 inline style 屬性(即 CSP Level 3 之 style-src-attr 子指令所管制之 style="" 屬性)。此類操作於移除 'unsafe-inline' 後會被 CSP 攔截,惟該函式庫對受攔截之 inline style 具備降級路徑,本次實測地圖底圖、圖層、標記及操作介面之渲染與互動完全正常。此為第三方函式庫之既有行為,不涉及本系統自有程式碼之注入面,建請列為可接受之殘餘風險。後續若有重大功能異動或第三方套件版本升級,將重新執行 CSP 稽核以確認相容性。

把「94」換成你自己實測的頁數,把「ArcGIS」換成你專案裡真正的元兇,這封信就能用。重點是:先做完實測,再寫這封信。實測數字是這封信唯一真正有殺傷力的部分。


寫到這裡,最該記下來的其實不是哪個 CSP 指令怎麼設。是那句「那豈不是只有原生能做到」。

我用 AI 寫 code 一年多,最大的體會是:它會用非常篤定的語氣,給你一個錯誤的結構性判斷。「57 個全死」「三大框架都一樣」這種話,它講得比我還有把握,還附上數字、附上橫向對比,聽起來無懈可擊。真正打破它的,往往不是更強的技術,而是一個聽起來很外行的常識追問——這怎麼可能全世界都這樣?

Claude Code 後來把整件事查得乾乾淨淨:94 頁實測、找出真兇、把給甲方的信也寫好。它確實很強。但那個「等一下,這不合理」的瞬間,目前還是得靠人。那 57 個被它判「全死」的 v-show,最後一個都沒死。