使用嚴格的內容安全政策 (CSP) 減少跨網站指令碼攻擊 (XSS)

Lukas Weichselbaum
Lukas Weichselbaum

Browser Support

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

跨網站指令碼攻擊 (XSS) 是指將惡意指令碼插入網頁應用程式的能力,十多年來一直是最大的網路安全漏洞之一。

內容安全政策 (CSP) 是額外的安全防護層,有助於降低 XSS 攻擊。如要設定 CSP,請在網頁中加入 Content-Security-Policy HTTP 標頭,並設定值來控管使用者代理程式可為該網頁載入哪些資源。

本頁說明如何使用以 Nonce 或雜湊為基礎的 CSP 來防範 XSS,而非使用常見的主機許可清單式 CSP,因為後者在大多數設定中都可略過,往往會使網頁暴露在 XSS 風險中。

重要詞彙:Nonce 是只使用一次的隨機號碼,可用來將 <script> 標記為可信任。

重要詞彙:雜湊函式是一種數學函式,可將輸入值轉換為壓縮的數值,稱為雜湊。您可以使用雜湊 (例如 SHA-256) 將內嵌 <script> 標記標示為受信任。

基於 Nonce 或雜湊的內容安全政策通常稱為「嚴格 CSP」。如果應用程式使用嚴格的 CSP,攻擊者即使發現 HTML 插入漏洞,通常也無法利用這些漏洞,強制瀏覽器在有安全漏洞的文件中執行惡意指令碼。這是因為嚴格的 CSP 只允許雜湊指令碼,或在伺服器上產生正確 Nonce 值的指令碼,因此攻擊者在不知道特定回應的正確 Nonce 時,無法執行指令碼。

為什麼要使用嚴格的 CSP?

如果網站已有類似 script-src www.googleapis.com 的 CSP,可能無法有效防範跨網站攻擊。這類 CSP 稱為「許可清單 CSP」。這類規則需要大量自訂,且攻擊者可以繞過。

基於密碼編譯 Nonce 或雜湊的嚴格 CSP 可避免這些問題。

嚴格 CSP 結構

基本的嚴格內容安全政策會使用下列其中一個 HTTP 回應標頭:

Nonce 型嚴格 CSP

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
以 Nonce 為基礎的嚴格 CSP 運作方式。

雜湊型嚴格 CSP

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

下列屬性可讓 CSP 變得「嚴格」,進而提高安全性:

  • 它會使用 Nonce 'nonce-{RANDOM}' 或雜湊 'sha256-{HASHED_INLINE_SCRIPT}',指出網站開發人員信任哪些 <script> 標記,可在使用者的瀏覽器中執行。
  • 這項指令會設定 'strict-dynamic',自動允許執行由信任指令碼建立的指令碼,減少部署 Nonce 或雜湊式 CSP 的工作。這也解除了大多數第三方 JavaScript 程式庫和 Widget 的使用限制。
  • 這項機制不以網址許可清單為依據,因此不會受到常見 CSP 略過的影響。
  • 這項政策會封鎖不受信任的內嵌指令碼,例如內嵌事件處理常式或 javascript: URI。
  • 這項政策會限制 object-src,停用 Flash 等危險外掛程式。
  • 這項政策會限制 base-uri,防止插入 <base> 標記。這可防止攻擊者變更從相對網址載入的指令碼位置。

採用嚴格 CSP

如要採用嚴格的 CSP,您必須:

  1. 決定應用程式是否應設定以 Nonce 或雜湊為基礎的 CSP。
  2. 複製「嚴格 CSP 結構」一節中的 CSP,並將其設為應用程式的回應標頭。
  3. 重構 HTML 範本和用戶端程式碼,移除與 CSP 不相容的模式。
  4. 部署 CSP。

在整個過程中,您可以使用 Lighthouse (v7.3.0 以上版本,並加上 --preset=experimental 旗標) 的最佳做法稽核,檢查網站是否設有 CSP,以及是否足夠嚴格,能有效防範 XSS。

Lighthouse 報表警告,指出在強制執行模式中找不到 CSP。
如果網站沒有 CSP,Lighthouse 就會顯示這則警告。

步驟 1:判斷是否需要以 Nonce 或雜湊為基礎的 CSP

以下說明這兩種嚴格 CSP 的運作方式:

Nonce 型 CSP

使用 Nonce 型 CSP 時,您會在執行階段產生隨機號碼,並將其納入 CSP,然後與網頁中的每個指令碼標記建立關聯。攻擊者無法在網頁中加入或執行惡意指令碼,因為他們必須猜出該指令碼的正確隨機號碼。只有在無法猜測數字,且每次回應時都會重新產生數字的情況下,這種做法才有效。

針對在伺服器上轉譯的 HTML 網頁,請使用以 Nonce 為基礎的 CSP。對於這類網頁,您可以為每個回應建立新的隨機號碼。

雜湊型 CSP

如果是雜湊式 CSP,每個內嵌指令碼標記的雜湊都會新增至 CSP。每個指令碼都有不同的雜湊。攻擊者無法在您的網頁中加入或執行惡意指令碼,因為該指令碼的雜湊必須位於 CSP 中才能執行。

如果 HTML 網頁是以靜態方式提供,或需要快取,請使用雜湊式 CSP。舉例來說,如果單頁網頁應用程式是以 Angular、React 等架構建構,且以靜態方式提供,不需伺服器端轉譯,您就可以使用雜湊式 CSP。

步驟 2:設定嚴格的 CSP 並準備指令碼

設定 CSP 時,有幾個選項:

  • 僅限報告模式 (Content-Security-Policy-Report-Only) 或強制執行模式 (Content-Security-Policy)。在僅限報告模式下,CSP 尚未封鎖資源,因此網站上的任何內容都不會中斷,但您可以查看錯誤,並取得任何會遭到封鎖的報告。在本機設定 CSP 時,這其實不重要,因為兩種模式都會在瀏覽器控制台中顯示錯誤。強制執行模式可協助您找出 CSP 草稿封鎖的資源,因為封鎖資源可能會導致網頁看起來不完整。在程序後續階段,僅限報表模式會變得最實用 (請參閱步驟 5)。
  • 標頭或 HTML <meta> 標記。在本機開發時,<meta> 標記可讓您更方便地調整 CSP,並快速查看對網站的影響。不過,請注意以下幾點:
    • 之後在正式環境中部署 CSP 時,建議將其設為 HTTP 標頭。
    • 如要將 CSP 設為僅限報表模式,請務必將其設為標頭,因為 CSP 中繼標記不支援僅限報表模式。

選項 A:Nonce 型 CSP

在應用程式中設定下列 Content-Security-Policy HTTP 回應標頭:

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

為 CSP 產生 Nonce

Nonce 是隨機號碼,每次載入網頁時只會使用一次。只有在攻擊者無法猜出 Nonce 值時,Nonce 型 CSP 才能防範 XSS。CSP Nonce 必須符合下列條件:

  • 加密強度高的隨機值 (長度最好超過 128 位元)
  • 每次回覆都會重新生成
  • Base64 編碼

以下舉例說明如何在伺服器端架構中新增 CSP Nonce:

const app = express();

app.get('/', function(request, response) {
  // Generate a new random nonce value for every response.
  const nonce = crypto.randomBytes(16).toString("base64");

  // Set the strict nonce-based CSP response header
  const csp = `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`;
  response.set("Content-Security-Policy", csp);

  // Every <script> tag in your application should set the `nonce` attribute to this value.
  response.render(template, { nonce: nonce });
});

在 <script> 元素中加入 nonce 屬性

使用 nonce 型 CSP 時,每個 <script> 元素都必須有 nonce 屬性,且該屬性必須與 CSP 標頭中指定的隨機 nonce 值相符。所有指令碼都可以使用相同的 Nonce。第一步是將這些屬性新增至所有指令碼,讓 CSP 允許這些指令碼。

做法 B:雜湊式 CSP 回應標頭

在應用程式中設定下列 Content-Security-Policy HTTP 回應標頭:

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

如有多個內嵌指令碼,語法如下:'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'。

動態載入來源指令碼

您可以使用內嵌指令碼動態載入第三方指令碼。

內嵌指令碼的範例。
CSP 允許
<script>
  var scripts = [ 'https://example.org/foo.js', 'https://example.org/bar.js'];

  scripts.forEach(function(scriptUrl) {
    var s = document.createElement('script');
    s.src = scriptUrl;
    s.async = false; // to preserve execution order
    document.head.appendChild(s);
  });
</script>
如要執行這個指令碼,您必須計算內嵌指令碼的雜湊,並將其新增至 CSP 回應標頭,取代 {HASHED_INLINE_SCRIPT} 預留位置。如要減少雜湊數量,可以將所有內嵌指令碼合併為單一指令碼。如要查看實際運作情形,請參閱這個範例和程式碼。
遭 CSP 封鎖
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
CSP 會封鎖這些指令碼,因為這些指令碼並非動態新增,且沒有與允許來源相符的 integrity 屬性。

指令碼載入注意事項

內嵌指令碼範例會新增 s.async = false,確保 foo 在 bar 之前執行,即使 bar 先載入也一樣。在這個程式碼片段中,s.async = false 不會在載入指令碼時封鎖剖析器,因為指令碼是動態新增。剖析器只會在指令碼執行時停止,就像 async 指令碼一樣。不過,使用這個程式碼片段時,請注意以下事項:

  • 其中一個或兩個指令碼可能會在文件下載完成前執行。如要讓文件在指令碼執行時準備就緒,請先等待 DOMContentLoaded 事件,再附加指令碼。如果指令碼未及早開始下載,導致效能問題,請在網頁前段使用預先載入標記。
  • defer = true 不會執行任何動作。如需這項行為,請在需要時手動執行指令碼。

步驟 3:重構 HTML 範本和用戶端程式碼

內嵌事件處理常式 (例如 onclick="…"、onerror="…") 和 JavaScript URI (<a href="javascript:…">) 可用於執行指令碼。這表示攻擊者只要找到 XSS 錯誤,就能插入這類 HTML 並執行惡意 JavaScript。以 Nonce 或雜湊為基礎的 CSP 禁止使用這類標記。如果您的網站使用上述任何模式,請務必重構,改用更安全的替代方案。

如果您在上一個步驟中啟用了 CSP,則每當 CSP 封鎖不相容的模式時,您都可以在控制台中看到 CSP 違規事項。

Chrome 開發人員控制台中的 CSP 違規報告。
阻斷程式碼的控制台錯誤。

在大多數情況下,修正方式很簡單:

重構內嵌事件處理常式

CSP 允許
<span id="things">A thing.</span>
<script nonce="${nonce}">
  document.getElementById('things').addEventListener('click', doThings);
</script>
CSP 允許使用 JavaScript 註冊的事件處理常式。
遭 CSP 封鎖
<span onclick="doThings();">A thing.</span>
CSP 會封鎖內嵌事件處理常式。

重構 javascript: 個 URI

CSP 允許
<a id="foo">foo</a>
<script nonce="${nonce}">
  document.getElementById('foo').addEventListener('click', linkClicked);
</script>
CSP 允許使用 JavaScript 註冊的事件處理常式。
遭 CSP 封鎖
<a href="javascript:linkClicked()">foo</a>
CSP 封鎖 javascript: URI。

從 JavaScript 中移除 eval()

如果應用程式使用 eval() 將 JSON 字串序列化轉換為 JS 物件,請將這類執行個體重構為 JSON.parse(),這樣也更快。

如果無法移除所有 eval() 用途,您仍可設定嚴格的 Nonce CSP,但必須使用 'unsafe-eval' CSP 關鍵字,這會稍微降低政策的安全性。

您可以在這個嚴格 CSP 程式碼實驗室中,找到這些範例和更多重構範例:

步驟 4 (選用):新增備用方案,支援舊版瀏覽器

Browser Support

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

如需支援舊版瀏覽器:

  • 如要使用 strict-dynamic,您必須新增 https: 做為舊版 Safari 的備用方案。這會造成如下影響:
    • 所有支援 strict-dynamic 的瀏覽器都會忽略 https: 備用機制,因此不會降低政策強度。
    • 在舊版瀏覽器中,外部來源指令碼只能從 HTTPS 來源載入。這種做法不如嚴格的 CSP 安全,但仍可防範一些常見的 XSS 原因,例如插入 javascript: URI。
  • 為確保與非常舊的瀏覽器版本 (4 年以上) 相容,您可以新增 unsafe-inline 做為備用。如果存在 CSP Nonce 或雜湊,所有新版瀏覽器都會忽略 unsafe-inline。
Content-Security-Policy:
  script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';

步驟 5:部署 CSP

確認 CSP 不會封鎖本機開發環境中的任何合法指令碼後,即可將 CSP 部署至測試環境,然後部署至正式環境:

  1. (選用) 使用 Content-Security-Policy-Report-Only 標頭,以僅限報表模式部署 CSP。在正式環境中,您可以使用僅限回報模式,測試可能造成破壞性變更 (例如新的 CSP),然後再開始強制執行 CSP 限制。在僅限報表模式下,CSP 不會影響應用程式的行為,但瀏覽器遇到與 CSP 不相容的模式時,仍會產生控制台錯誤和違規報告,方便您瞭解使用者會遇到什麼問題。詳情請參閱「Reporting API」。
  2. 確認 CSP 不會破壞網站的使用者體驗後,請使用 Content-Security-Policy 回應標頭部署 CSP。建議您使用 HTTP 標頭伺服器端設定 CSP,因為這比 <meta> 標記更安全。完成這個步驟後,CSP 就會開始保護應用程式免受 XSS 攻擊。

限制

嚴格的 CSP 通常會提供強大的額外安全防護,有助於防範 XSS。在大多數情況下,CSP 會拒絕 javascript: URI 等危險模式,大幅減少攻擊面。不過,視您使用的 CSP 類型 (Nonce、雜湊、是否包含 'strict-dynamic') 而定,在某些情況下,CSP 可能無法妥善保護應用程式:

  • 如果您對指令碼使用 Nonce,但有插入內容直接進入該 <script> 元素的內文或 src 參數。
  • 如果動態建立的指令碼位置 (document.createElement('script')) 遭到插入,包括根據引數值建立 script DOM 節點的任何程式庫函式。這包括一些常見的 API,例如 jQuery 的 .html(),以及 jQuery < 3.0 中的 .get() 和 .post()。
  • 如果舊版 AngularJS 應用程式中有範本插入,如果攻擊者能插入 AngularJS 範本,就能用來執行任意 JavaScript。
  • 如果政策包含 'unsafe-eval',則會插入 eval()、setTimeout() 和其他少數不常使用的 API。

開發人員和資安工程師在程式碼審查和安全性稽核期間,應特別留意這類模式。如要進一步瞭解這些案例,請參閱「內容安全政策:強化與緩解之間的成功混亂」。

延伸閱讀