跨網站指令碼攻擊 (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';
以雜湊為基礎的嚴格 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,您需要:
- 決定您的應用程式應設定 Nonce 或雜湊式 CSP。
- 複製「嚴格 CSP 結構」部分中的 CSP,並將其設為整個應用程式的回應標頭。
- 重構 HTML 範本和用戶端程式碼,移除與 CSP 不相容的模式。
- 部署 CSP。
您可以在整個過程中使用 Lighthouse (v7.3.0 及以上版本並搭配 --preset=experimental
旗標) 最佳做法稽核,檢查您的網站是否具有 CSP,以及審查作業是否夠嚴格,足以抵禦 XSS。
步驟 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 中繼標記不支援「僅限報表」模式,您必須將其設為標頭。
在應用程式中設定下列 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 的範例:
- 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("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 允許這些屬性。
在應用程式中設定下列 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>
<script src="https://example.org/foo.js"></script> <script src="https://example.org/bar.js"></script>
指令碼載入註意事項
內嵌指令碼範例會加入 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 違規事項。
在大多數情況下,修正方式非常簡單:
重構內嵌事件處理常式
<span id="things">A thing.</span> <script nonce="${nonce}"> document.getElementById('things').addEventListener('click', doThings); </script>
<span onclick="doThings();">A thing.</span>
重構 javascript:
URI
<a id="foo">foo</a> <script nonce="${nonce}"> document.getElementById('foo').addEventListener('click', linkClicked); </script>
<a href="javascript:linkClicked()">foo</a>
從 JavaScript 中移除 eval()
如果應用程式使用 eval()
將 JSON 字串序列化作業轉換為 JS 物件,您應該將這類例項重構為 JSON.parse()
,這會更快完成這項程序。
如果無法移除 eval()
的所有用途,您仍可設定嚴格以 Nonce 為基礎的 CSP,但必須使用 'unsafe-eval'
CSP 關鍵字,這會使政策的安全性略低。
您可以在這個嚴謹的 CSP 程式碼研究室中找到下列以及更多此類重構的範例:
步驟 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 部署至測試環境,然後部署至實際工作環境:
- (選用) 使用
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')
) 的位置插入內容,包括任何根據相關引數的值建立script
DOM 節點的程式庫函式。包括一些常見的 API,例如 jQuery 的.html()
,以及 jQuery 3.0 < 3.0 中的.get()
和.post()
。 - 如果舊版 AngularJS 應用程式有插入範本插入項目,可插入 AngularJS 範本的攻擊者就能使用這個範本執行任意 JavaScript。
- 如果政策包含
'unsafe-eval'
,則插入eval()
、setTimeout()
和其他一些很少使用的 API。
在程式碼審查和安全性稽核期間,開發人員和安全性工程師應特別留意這類模式。您可以參閱「內容安全政策:在強化和緩解之間成功的方法」一文,進一步瞭解這些案例。