Browser Support
跨網站指令碼攻擊 (XSS) 是指將惡意指令碼注入網頁應用程式的能力,這十多年來一直是最大的網頁安全漏洞之一。
內容安全政策 (CSP)
是額外的安全防護層,有助於減輕 XSS 攻擊。如要設定 CSP,請在網頁中新增 Content-Security-Policy HTTP 標頭,並設定值來控管使用者代理程式可為該網頁載入的資源。
本頁說明如何使用以隨機數或雜湊為基礎的 CSP 減輕 XSS 攻擊,而不是使用常見的以主機允許清單為基礎的 CSP,因為這類 CSP 在大多數設定中都能規避,因此網頁經常會暴露於 XSS 攻擊。
重要字詞:Nonce是只能使用一次的隨機數字,可用於將 <script> 標記為信任。
重要術語:雜湊函式是一種數學函式,可將輸入值轉換為稱為「雜湊」的壓縮數值。您可以使用雜湊 (例如 SHA-256) 將內嵌 <script> 標記為可信任。
以隨機數或雜湊值為依據的內容安全政策,通常稱為「嚴格 CSP」。如果應用程式使用嚴格的 CSP,攻擊者即使發現 HTML 注入缺陷,通常也無法利用這些缺陷,強制瀏覽器在有安全漏洞的文件中執行惡意指令碼。這是因為嚴格的 CSP 只允許使用雜湊指令碼或具有伺服器上產生的正確 Nonce 數值的指令碼,因此攻擊者無法在不知道特定回應的正確 Nonce 數值的情況下執行指令碼。
為什麼應該使用嚴格的 CSP?
如果您的網站已有類似 script-src www.googleapis.com 的 CSP,可能無法有效防範跨網站攻擊。這類 CSP 稱為「允許清單 CSP」。這類驗證方式需要大量自訂作業,而且攻擊者可以繞過驗證。
以密碼編譯隨機數或雜湊為依據的嚴格 CSP 可避免這些陷阱。
嚴格 CSP 結構
基本的嚴格內容安全政策會使用下列其中一個 HTTP 回應標頭:
以 Nonce 為基礎的嚴格 CSP
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none39;;
base-uri 'none';
雜湊型嚴格 CSP
Content-Security-Policy:
script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
object-src 'none39;;
base-uri 'none';
下列屬性可讓 CSP 成為「嚴格」的 CSP,因此更安全:
- 這項政策會使用隨機數
'nonce-{RANDOM}'或雜湊'sha256-{HASHED_INLINE_SCRIPT}',指出網站開發人員信任哪些<script>標記,允許在使用者瀏覽器中執行。 - 這項功能會設定
'strict-dynamic',自動允許執行受信任指令碼建立的指令碼,藉此減少部署以隨機數或雜湊為基礎的 CSP 時所需的工作量。這也會解除大多數第三方 JavaScript 程式庫和 Widget 的使用限制。 - 這項機制並非以網址允許清單為基礎,因此不會受到常見 CSP 繞過方式影響。
- 這會封鎖不安全的內嵌指令碼,例如內嵌事件處理常式或
javascript:URI。 - 這項設定會限制
object-src停用 Flash 等危險外掛程式。 - 這項限制會禁止注入
<base>代碼。base-uri這可防止攻擊者變更從相對網址載入的指令碼位置。
採用嚴格 CSP
如要採用嚴格的 CSP,請按照下列步驟操作:
- 決定應用程式是否應設定以 Nonce 或 Hash 為基礎的 CSP。
- 從「嚴格 CSP 結構」部分複製 CSP,並在應用程式中將其設為回應標頭。
- 重構 HTML 範本和用戶端程式碼,移除與 CSP 不相容的模式。
- 部署 CSP。
您可以在這個過程中,使用 Lighthouse (第 7.3.0 版以上,並搭配 --preset=experimental 標記) Best Practices 稽核,檢查網站是否設有 CSP,以及 CSP 是否夠嚴格,能有效防範跨網站指令碼攻擊。
步驟 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 時,建議您將其設為 HTTP 標頭。
- 如要以僅回報模式設定 CSP,請將其設為標頭,因為 CSP 中繼標記不支援僅回報模式。
在應用程式中設定下列 Content-Security-Policy HTTP 回應標頭:
Content-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none';
產生 CSP 的 Nonce
Nonce 是隨機數字,每次載入網頁時只會使用一次。只有在攻擊者無法猜出隨機碼值的情況下,以隨機碼為基礎的 CSP 才能防範 XSS。A CSP 隨機值必須符合下列條件:
- 加密強度高的隨機值 (理想長度為 128 位元以上)
- 每次回覆都會重新生成
- Base64 編碼
以下範例說明如何在伺服器端架構中新增 CSP 隨機值:
- Django (Python)
- Express (JavaScript):
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(&>quot;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 標頭中指定的隨機數值相符。所有指令碼都可以使用相同的隨機數。第一步是在所有指令碼中加入這些屬性,讓 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}'。
動態載入來源指令碼
您可以使用內嵌指令碼動態載入第三方指令碼。
<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.hea<d.appen>dChild(s);
});
/script{HASHED_INLINE_SCRIPT} 預留位置。如要減少雜湊數量,您可以將所有內嵌指令碼合併為單一指令碼。如要查看實際運作情形,請參閱這個範例及其程式碼。
<script src="https://example.org/fo><o.js&qu>o<t;/script script src="https://exam><ple.org>/bar.js"/script
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。以隨機數或雜湊為基礎的 CSP 會禁止使用這類標記。
如果您的網站使用任何這類模式,請將其重構為更安全的替代方案。
如果您在上一個步驟中啟用 CSP,每當 CSP 封鎖不相容的模式時,您就能在控制台中看到 CSP 違規事項。
在大多數情況下,修正方式很簡單:
重構內嵌事件處理常式
<span id="th>ings&quo<t;A t>h<ing./span
script nonce=>"${nonce}"
document.getElementById('things').addEventL<istener>('click', doThings);
/script<span onclick="doThing>s();&quo<t;A t>hing./span
重構 javascript: URI
<a id=">;fo<o&>q<uot;foo/a
script nonce=>"${nonce}"
document.getElementById('foo').addEventList<ener(>39;click', linkClicked);
/script<a href="javascript:linkClick>ed(<)&>quot;foo/a
從 JavaScript 中移除「eval()」
如果應用程式使用 eval() 將 JSON 字串序列化轉換為 JS 物件,您應將這類執行個體重構為 JSON.parse(),這樣做也更快。
如果無法移除所有 eval() 的用法,您還是可以設定嚴格的 nonce 型 CSP,但必須使用 'unsafe-eval' CSP 關鍵字,這會讓政策的安全性稍有不足。
如要查看這類重構的範例,請參閱這個嚴格 CSP 程式碼研究室:
步驟 4 (選用):新增備援,支援舊版瀏覽器
Browser Support
如需支援舊版瀏覽器:
- 使用
strict-dynamic時,必須為舊版 Safari 新增https:做為備用項目。這會造成如下影響:- 所有支援
strict-dynamic的瀏覽器都會忽略https:後備字型, 因此不會降低政策的效力。 - 在舊版瀏覽器中,只有來自 HTTPS 來源的外部來源指令碼才能載入。這比嚴格的 CSP 安全性較低,但仍可避免一些常見的 XSS 原因,例如注入
javascript:URI。
- 所有支援
- 如要確保與非常舊的瀏覽器版本 (4 年以上) 相容,可以新增
unsafe-inline做為備用字型。所有最新版瀏覽器都會忽略unsafe-inline,前提是存在 CSP Nonce 或雜湊值。
Content-Security-Policy:
script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
object-src 9;none';
base-uri 'none';
步驟 5:部署 CSP
確認 CSP 不會封鎖本機開發環境中的任何合法指令碼後,即可將 CSP 部署至預先發布環境,然後部署至正式環境:
- (選用) 使用
Content-Security-Policy-Report-Only標頭,以僅回報模式部署 CSP。在開始強制執行 CSP 限制之前,您可以在正式環境中測試可能造成中斷的變更 (例如新的 CSP),這時唯讀模式就派上用場了。在僅回報模式下,CSP 不會影響應用程式的行為,但瀏覽器遇到與 CSP 不相容的模式時,仍會產生主控台錯誤和違規報告,因此您可以查看使用者會遇到的問題。詳情請參閱「Reporting API」。 - 確認 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')) 遭到注入,包括根據引數值建立scriptDOM 節點的任何程式庫函式。這包括一些常見的 API,例如 jQuery 的.html(),以及 jQuery 3.0 之前的.get()和.post()。 - 如果舊版 AngularJS 應用程式中含有範本注入內容,如果攻擊者能將程式碼注入 AngularJS 範本,就能利用該範本執行任意 JavaScript。
- 如果政策包含
'unsafe-eval',則會注入eval()、setTimeout()和其他幾個不常用的 API。
開發人員和安全防護工程師在進行程式碼審查和安全稽核時,應特別留意這類模式。如要進一步瞭解這些案例,請參閱「內容安全政策:強化與緩解之間的成功混亂」。
。