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

Lukas Weichselbaum
Lukas Weichselbaum

瀏覽器支援

  • 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 程式庫和小工具的封鎖。
  • 這個設定不是以網址許可清單為依據,因此不受常見的 CSP 略過
  • 它會封鎖不受信任的內嵌指令碼,例如內嵌事件處理常式或 javascript: URI。
  • 這項政策會限制 object-src 停用危險的外掛程式,例如 Flash。
  • 這項設定會限制 base-uri,以阻止 <base> 代碼的注入。這可防止攻擊者變更從相對網址載入的指令碼位置。

採用嚴格 CSP

如要採用嚴格 CSP,您需要:

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

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

Lighthouse 報告警告:在強制執行模式下找不到 CSP。
如果您的網站沒有 CSP,Lighthouse 會顯示這項警告。

步驟 1:決定是否需要 Nonce 或 Hash 的 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 對網站的影響。不過,請注意以下幾點:
    • 日後在正式環境中部署 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 為基礎的 CSP 才能減輕 XSS 的風險。CSP 隨機字串必須符合下列條件:

  • 經過加密的高強度隨機值 (最好長度為 128 位元以上)
  • 針對每個回應產生
  • Base64 編碼

以下列舉幾個在伺服器端架構中新增 CSP 隨機字串的範例:

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,確保 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 (選用):新增備用方案以支援舊版瀏覽器

瀏覽器支援

  • Chrome:52。
  • Edge:79,
  • Firefox:52。
  • Safari:15.4。

資料來源

如果您需要支援舊版瀏覽器:

  • 使用 strict-dynamic 時,必須為舊版 Safari 新增 https: 做為預設值。步驟如下:
    • 所有支援 strict-dynamic 的瀏覽器都會忽略 https: 備用方案,因此這不會降低政策的強度。
    • 在舊版瀏覽器中,只有來自 HTTPS 來源的外部來源指令碼才能載入。這種做法較不安全,但仍可防止一些常見的 XSS 攻擊,例如注入 javascript: URI。
  • 為確保與非常舊的瀏覽器版本 (4 年以上) 相容,您可以新增 unsafe-inline 做為備用方案。如果存在 CSP 隨機值或雜湊碼,所有近期的瀏覽器都會忽略 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、Hash、是否含有 '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。

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

延伸閱讀