The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

套用 CSP (Content Security Policy) 的網站要如何安全的使用 Inline script

我有個客戶的網站最近剛上線,在設定 CSP (Content Security Policy) 標頭的時候,因為我設定了 unsafe-inline 的關係,所以導致 Google Analytics (GA) 的程式無法執行,但 GA 網站提供的程式碼就是用 Inline Script (內嵌腳本) 怎麼辦呢?這篇文章我就來告訴你解決方案。

A wide banner image illustrating the concept of Content Security Policy (CSP) in website security, with visual metaphors for implementing inline script

首先,我們先來看看從 Google Analytics 網站上取得的 GTM (Google Tag Manager) 程式碼:

<!-- Google tag (gtag.js) -->
<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>

基本上,這段程式碼的第二段 <script> 就是一個 Inline Script,如果我們要遵守 CSP 的規範,就不要加上 unsafe-inline 這個設定,否則網站就容易遭受到 XSS 攻擊,那該怎麼辦呢?

這時你有兩種選擇可以實作:

  1. 使用 nonce
  2. 使用 hash

這兩種方法各有優缺點,然而 nonce 的實作比 hash 複雜些,但相對的就比較安全。而 hash 的實作比較簡單,但相對的在維護上就比較麻煩。

使用 nonce

先來看看使用 nonce 的 CSP Header 長怎樣:

Content-Security-Policy: script-src 'nonce-{SERVER-GENERATED-NONCE}'; img-src www.googletagmanager.com

基本上麻煩的點在於,你的每一個 Request 都要產生一組唯一的 Nonce 亂數值,然後套用在 CSP 標頭中,如上述 {SERVER-GENERATED-NONCE} 部分。接著,你還要在你網頁中的 Inline Script 加上 nonce 屬性,兩邊的值必須完全相等,而且每個 Request 都不能相同,才算是一個合格的設定:

<script nonce='{SERVER-GENERATED-NONCE}'>
  ...
</script>

說起來簡單,但麻煩的點就在於需要後端產生 Nonce 的功能,亂數產生器還要有一定的加密強度,而且你的所有 Requests 都不能實作「回應快取」(Response Cache),否則這些 Inline Script 都會無法執行,因為 CSP 要求每個 Request 都要產生一組唯一的 Nonce 亂數值,這樣才能達到安全的效果。

使用 hash

如果對於 SPA (Single-page application) 類型的程式,或是網站部署在 GitHub Pages 這種只能放靜態檔案的網站伺服器,你可以考慮使用 hash 的方式來實作 CSP 的設定,這樣就不需要後端產生 Nonce 亂數值,而且也不需要禁用快取功能。

先假設你的 Inline script 定義如下:

<script>
  window.dataLayer = window.dataLayer || [];
  function gtag() {
    dataLayer.push(arguments);
  }
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXXXXX');
</script>

接著你要先將 <script> 直到 </script> 之間的內容進行 SHA256 的演算法計算,這樣就可以得到一組 Hash 值。我們可以利用 SHA256 - Online Tools 線上工具幫我們計算。例如上述 Inline Script 的 SHA256 雜湊值就是以下這個:

0bbd063b7a40529717ebe6082ce3f8d882d18c6d930fe4fec0c862b0dd0f9851

理論上,任何一段相同的內容所計算出來的雜湊值都必定相同。但是使用 hash 最雷的地方,就在於大家算出來的 SHA256 可能會有點不太一樣!因為從 <script> 直到 </script> 之間的內容,其實還包含了不可見的字元,例如斷行字元。有些人的檔案,斷行符號是 LF (\n),但有些人是 CRLF (\r\n),就這點小差異就有可能讓你的 CSP 設定失效,這點務必小心。

接著我們將上述結果設定到 CSP 標頭中,就大功告成了:

Content-Security-Policy: script-src 'sha256-{HASHED-INLINE-SCRIPT}'; img-src www.googletagmanager.com

你只要把 Inline Script 的內容透過 SHA256 的演算法計算出 Hash 值,然後套用在 CSP 標頭中,如上述 {HASHED-INLINE-SCRIPT} 部分,就大功告成啦!

不過,這種設定方法有個小缺點,每次 Inline Script 的內容有變動時,你都必須重新計算一次 Hash 值,然後更新 CSP 標頭的設定,這樣才能讓 CSP 設定生效。假設後續接手維護該網站的人,不知道你的網站有設定 CSP 的話,他只要修改了 Inline Script 的內容,就會導致網頁無法正常運作,因為 CSP 會阻擋這些 hash 設定不一致的 Inline script,這點也要特別注意。

同場加映

如果要讓網頁更安全,你還可以在網頁中的 External Script 加上 integrity 屬性,這個屬性的值必須跟 CSP 標頭上的 Hash 設定完全相等,才算是一個正確無誤且沒有被竄改的外部載入腳本版本。

Content-Security-Policy: script-src 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC' 'sha256-fictional_value'
<script
  src="https://example.com/example-framework.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"></script>

詳見 Whitelisting external scripts using hashes 文章。

總結

這篇文章你可以發現,使用 noncehash 各有優缺點,但基本上都能增強網站的安全性。

以下是這兩種方式的優缺點整理:

  nonce hash
優點 腳本內容維護時,CSP 標頭不用改 設定簡單,無須後端支援。
缺點 需後端支援,且每個 Request 都要產生唯一的 nonce 腳本內容維護時,CSP 標頭要更新

相關連結

留言評論