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

Lukas Weichselbaum
Lukas Weichselbaum

瀏覽器支援

  • 52
  • 79
  • 52
  • 15.4

資料來源

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

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

本頁面說明如何使用以 Nonce 或雜湊為基礎的 CSP 來緩解 XSS,而非常用的以主機許可清單為基礎的 CSP,而 CSP 通常離開頁面並暴露於 XSS,因為這類 CSP 可以在多數設定中略過

關鍵字詞: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 程式庫和小工具。
  • 並非根據網址許可清單,因此不受常見的 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。舉例來說,您可以將雜湊式 CSP 用於使用 Angular、React 等架構建構的單頁網頁應用程式,這類應用程式在沒有伺服器端轉譯的情況下靜態提供。

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

設定 CSP 時,有以下幾種選擇:

  • 僅限報表模式 (Content-Security-Policy-Report-Only) 或強制執行模式 (Content-Security-Policy)。在僅限報表模式下,CSP 不會封鎖資源,因此網站不會發生任何中斷情形,但會顯示錯誤並產生任何已封鎖項目的報告。在本機設定 CSP 時並不重要,因為這兩種模式都會顯示在瀏覽器主控台中的錯誤。如果有任何項目,強制執行模式可協助您找到 CSP 區塊草稿的資源,因為封鎖資源可能會導致頁面看起來毀損。報表專用模式在後續程序中最為實用 (請參閱步驟 5)。
  • 標頭或 HTML <meta> 標記。針對本機開發作業,<meta> 標記可能更方便調整 CSP 並快速查看對網站造成的影響。不過,請注意以下幾點:
    • 您之後在實際工作環境中部署 CSP 時,我們建議將其設為 HTTP 標頭。
    • 如果 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 雜湊,因此您必須使用內嵌指令碼以動態方式載入所有第三方指令碼。不同瀏覽器不支援來源指令碼雜湊。

如何內嵌指令碼的範例。
由 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 會封鎖這些指令碼,因為只有內嵌指令碼才能進行雜湊處理。

指令碼載入註意事項

內嵌指令碼範例會加入 s.async = false,確保 foobar 之前執行 (即使先載入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 (選用):新增備用選項,支援舊版瀏覽器

瀏覽器支援

  • 52
  • 79
  • 52
  • 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 也無法保護您的應用程式:

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

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

其他資訊