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

我有個客戶的網站最近剛上線，在設定 CSP ([Content Security Policy](https://content-security-policy.com/)) 標頭的時候，因為我設定了 `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](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/2b3ddda0be9684dba0ca-155abb43-8ff5-480f-b6dd-89e5f644d883.webp)

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

```html
<!-- 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 都不能相同，才算是一個合格的設定：

```html
<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 定義如下：

```html
<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](https://emn178.github.io/online-tools/sha256.html) 線上工具幫我們計算。例如上述 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'
```

```html
<script
  src="https://example.com/example-framework.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"></script>
```

詳見 [Whitelisting external scripts using hashes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#whitelisting_external_scripts_using_hashes) 文章。

### 總結

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

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

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

### 相關連結

-   HTTP | MDN
    -   [CSP source values](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/Sources)
    -   [CSP: script-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src)
-   好用的 .NET 套件
    -   [Add CSP, HSTS or HPKP headers to an ASP.NET Core app](https://github.com/juunas11/aspnetcore-security-headers) ([NuGet](https://www.nuget.org/packages/Joonasw.AspNetCore.SecurityHeaders))
-   Google Analytics
    -   [Use Tag Manager with a Content Security Policy | Security and Privacy hub | Google for Developers](https://developers.google.com/tag-platform/security/guides/csp?hl=en)
-   [Content-Security-Policy (CSP) Header Quick Reference](https://content-security-policy.com/)
