當你在幫網站做健康檢查,看到弱掃報告( 像是 ZAP )跳出一個橘黃色的 Medium 警告,寫著 style-src 'unsafe-inline' 或是發現 script-src 滿是漏洞時,心裡是不是頓時涼了一半?
簡單來說,如果你的 CSP ( 網頁安全政策 )開了 unsafe-inline ,就等於是在大門裝了密碼鎖,卻把密碼用便利貼貼在門口一樣。駭客只要找到機會塞一段惡意腳本( XSS 攻擊 ),你的網站就直接變成他的遊樂場。
但是偏偏像是 Google Tag Manager ( GTM )這種必裝的分析工具,官方給的程式碼裡面就是有一大段 Inline Script :
HTML
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>
既想要防禦 XSS 攻擊,又不能把 GTM 拔掉,這時候你有兩條路可以走,讓我們來看看這兩個相愛相殺的解決方案。
方案一:使用 Nonce ( 一次性密碼鎖 )
這個方法就像是去看演唱會,每次進場工作人員都會在你的手上蓋一個當天限定、過期失效的印章。
在 CSP 標頭( Header )裡,你可以這樣設定:
HTTP
Content-Security-Policy: script-src 'nonce-每天換的神秘亂數密碼'; img-src www.googletagmanager.com
然後在你的網頁程式碼中,那段 GTM 的標籤也必須帶上相同的密碼:
HTML
<script nonce='每天換的神秘亂數密碼'>
// GTM 的扣(Code)放這裡
</script>
💡 工程師真心話
- 優點:超級安全。因為駭客根本猜不到你下一個 Request 的亂數密碼是什麼,就算他成功塞了惡意指令,也會因為沒有通關密碼而被瀏覽器直接擋在門外。
- 缺點( 也就是坑 ):這非常考驗後端的大腦。你的伺服器必須在「 每次 」收到請求時,都用高強度的加密演算法生出一組全新的隨機字串。最慘的是,你的網頁從此跟「 回應快取 」( Response Cache )說再見,因為一旦網頁被快取,密碼就不會變了,那這個鎖就形同虛設。
方案二:使用 Hash ( 驗收指紋認證 )
如果你的網站是 SPA ( 單頁應用程式 ),或是部署在 GitHub Pages 這種根本沒有後端可以幫忙生密碼的靜態伺服器,那 Hash 就是你的救星。
這個概念是,瀏覽器會直接去核對這段程式碼的「 指紋 」。
步驟非常簡單,把 <script> 到 </script> 之間的程式碼內容,丟到 SHA256 線上計算工具 裡面去跑一下,會得到一串像是 0bbd063b7... 的雜湊值( Hash )。
接著把這串指紋放進你的 CSP 設定裡:
HTTP
Content-Security-Policy: script-src 'sha256-計算出來的那串指紋'; img-src www.googletagmanager.com
💡 工程師真心話
- 優點:前端自己就能搞定,不需要後端伺服器配合,而且網頁依然可以開開心心地做快取,提升載入速度。
- 缺點( 最雷的地方 ):
- 字元敏感度破表:SHA256 是一個極度龜毛的演算法。只要你的程式碼裡面多了一個空格、少了一個換行,甚至不同工程師的電腦存檔時,斷行符號一個是 LF (
\n)一個是 CRLF (\r\n),算出來的指紋就完全不一樣。指紋一對不上,你的 GTM 就會直接被 CSP 射下來。 - 維護地獄:哪天行銷團隊突然要求修改 GTM 裡面的參數,只要動到一個字,你就必須重新計算一次 Hash 值、重新更新 CSP 標頭。如果交接沒做好,後續接手的工程師改了程式碼發現網站功能壞掉,可能要在電腦前哭著找兇手找很久。
- 字元敏感度破表:SHA256 是一個極度龜毛的演算法。只要你的程式碼裡面多了一個空格、少了一個換行,甚至不同工程師的電腦存檔時,斷行符號一個是 LF (
總結防禦指南
想要高枕無憂、後端架構也撐得住,請選 Nonce 。
如果是靜態網站、追求速度與快取,請選 Hash ,但每次改動程式碼時,請記得去燒香拜拜順便更新指紋。
總之,快把 unsafe-inline 從你的 CSP 裡面拿掉,別再讓你的網站裸奔了。