內容安全策略(CSP)

內容安全策略 (CSP) 是一項有助於預防或最大限度地降低某些型別安全威脅風險的功能。它由網站向瀏覽器發出的一系列指令組成,這些指令指示瀏覽器限制構成網站的程式碼所允許執行的操作。

CSP 的主要用例是控制文件允許載入哪些資源,特別是 JavaScript 資源。這主要用作抵禦跨站指令碼 (XSS) 攻擊的防禦措施,攻擊者能夠將惡意程式碼注入受害者的網站。

CSP 也可以有其他用途,包括防禦點選劫持,並幫助確保網站頁面透過 HTTPS 載入。

在本指南中,我們將首先描述 CSP 如何傳遞給瀏覽器,以及它在高層面上是什麼樣子。

然後,我們將描述如何使用它來控制載入的資源以防範 XSS,以及其他用例,例如點選劫持保護升級不安全請求。請注意,不同的用例之間沒有依賴關係:如果你想新增點選劫持保護但不想緩解 XSS,你可以只新增該用例的指令。

最後,我們將描述部署 CSP 的策略以及有助於簡化此過程的工具。

CSP 概述

CSP 應在 Content-Security-Policy 響應頭中傳遞給瀏覽器。它應該在所有請求的所有響應上設定,而不僅僅是主文件。

你也可以使用文件 <meta> 元素的 http-equiv 屬性來指定它,這對於某些用例來說是一個有用的選項,例如只有靜態資源的客戶端渲染的單頁應用程式,因為這樣你就可以避免依賴任何伺服器基礎設施。但是,此選項不支援所有 CSP 功能。

策略以一系列以分號分隔的指令的形式指定。每個指令控制安全策略的不同方面。每個指令都有一個名稱,後跟一個空格,再後跟一個值。不同的指令可以有不同的語法。

例如,考慮以下 CSP

http
Content-Security-Policy: default-src 'self'; img-src 'self' example.com

它設定了兩個指令

  • default-src 指令設定為 'self'
  • img-src 指令設定為 'self' example.com

A CSP broken into its directives.

第一個指令 default-src 告訴瀏覽器只加載與文件同源的資源,除非其他更具體的指令為其他資源型別設定了不同的策略。第二個指令 img-src 告訴瀏覽器載入與文件同源或從 example.com 提供的影像。

在下一節中,我們將介紹可用於控制資源載入的工具,這是 CSP 的主要功能。

控制資源載入

CSP 可用於控制文件允許載入的資源。這主要用於防範跨站指令碼 (XSS) 攻擊。

在本節中,我們將首先了解控制資源載入如何幫助防範 XSS,然後瞭解 CSP 提供的控制載入哪些資源的工具。最後,我們將描述一種特別推薦的策略,稱為“嚴格 CSP”。

XSS 和資源載入

跨站指令碼 (XSS) 攻擊是指攻擊者能夠在目標網站的上下文中執行其程式碼。然後,此程式碼能夠執行網站自身程式碼可以執行的任何操作,例如

  • 訪問或修改網站已載入頁面的內容
  • 訪問或修改本地儲存中的內容
  • 使用使用者憑據發出 HTTP 請求,使其能夠冒充使用者或訪問敏感資料

當網站接受可能由攻擊者精心製作的某些輸入(例如,URL 引數或部落格文章上的評論)然後將其包含在頁面中而沒有淨化它時,XSS 攻擊是可能的:也就是說,沒有確保它不能作為 JavaScript 執行。

網站應透過在將此輸入包含在頁面中之前對其進行淨化來保護自己免受 XSS 的侵害。CSP 提供了補充保護,即使淨化失敗,它也能保護網站。

如果淨化確實失敗,注入的惡意程式碼可以在文件中採取各種形式,包括

  • 連結到惡意源的 <script> 標籤

    html
    <script src="https://evil.example.com/hacker.js"></script>
    
  • 包含內聯 JavaScript 的 <script> 標籤

    html
    <script>
      console.log("You've been hacked!");
    </script>
    
  • 內聯事件處理程式

    html
    <img onmouseover="console.log(`You've been hacked!`)" />
    
  • javascript: URL

    html
    <iframe src="javascript:console.log(`You've been hacked!`)"></iframe>
    
  • 不安全 API(如 eval())的字串引數

    js
    eval("console.log(`You've been hacked!`)");
    

CSP 可以提供針對所有這些的保護。使用 CSP,你可以

  • 定義 JavaScript 檔案和其他資源的允許源,有效地阻止從 https://evil.example.com 載入
  • 停用內聯指令碼標籤
  • 只允許設定了正確 nonce 或 hash 的指令碼標籤
  • 停用內聯事件處理程式
  • 停用 javascript: URL
  • 停用危險 API,如 eval()

在下一節中,我們將介紹 CSP 為執行這些操作而提供的工具。

注意:設定 CSP 不能替代淨化輸入。網站應該淨化輸入設定 CSP,從而為 XSS 提供深度防禦。

Fetch 指令

Fetch 指令用於指定文件允許載入的特定類別的資源——例如 JavaScript、CSS 樣式表、影像、字型等。

不同型別的資源有不同的 fetch 指令。例如

一個特殊的 fetch 指令是 default-src,它為所有未明確列出其指令的資源設定回退策略。

有關完整的 fetch 指令集,請參閱參考文件

每個 fetch 指令都指定為單個關鍵字 'none' 或一個或多個以空格分隔的源表示式。當列出多個源表示式時:如果任何方法允許該資源,則該資源被允許。

例如,下面的 CSP 設定了兩個 fetch 指令

  • default-src 具有單個源表示式 'self'
  • img-src 具有兩個源表示式:'self'example.com

CSP diagram showing source expressions

這會產生以下效果

  • 影像必須與文件同源,或者從 example.com 載入
  • 所有其他資源必須與文件同源。

在接下來的幾節中,我們將描述一些使用源表示式控制資源載入的方法。請注意,儘管我們分別描述它們,但這些表示式通常可以組合:例如,單個 fetch 指令可能包含 nonce 和主機名。

阻止資源

要完全阻止某種資源型別,請使用 'none' 關鍵字。例如,以下指令阻止所有 <object><embed> 資源

http
Content-Security-Policy: object-src 'none'

請注意,'none' 不能與特定指令中的任何其他方法結合使用:實際上,如果 alongside 'none' 提供了任何其他源表示式,則它們將被忽略。

隨機數

nonce 是限制 <script><style> 資源載入的推薦方法。

使用 nonce,伺服器為每個 HTTP 響應生成一個隨機值,並將其包含在 script-src 和/或 style-src 指令中

http
Content-Security-Policy:
  script-src 'nonce-416d1177-4d12-4e3b-b7c9-f6c409789fb8'

然後,伺服器將此值作為它們打算包含在文件中的所有 <script> 和/或 <style> 標籤的 nonce 屬性的值。

瀏覽器比較這兩個值,並且僅當它們匹配時才載入資源。其思想是,即使攻擊者可以將一些 JavaScript 插入頁面,他們也不知道伺服器將使用哪個 nonce,因此瀏覽器將拒絕執行指令碼。

要使此方法奏效,攻擊者必須無法猜測 nonce。

實際上,這意味著 nonce 必須對每個 HTTP 響應都不同,並且不能可預測。

這反過來意味著伺服器不能提供靜態 HTML,因為它每次都必須插入新的 nonce。通常,伺服器會使用模板引擎來插入 nonce。

這是一個 Express 程式碼片段,用於演示

js
function content(nonce) {
  return `
    <script nonce="${nonce}" src="/main.js"></script>
    <script nonce="${nonce}">console.log("hello!");</script>
    <h1>Hello world</h1> 
    `;
}

app.get("/", (req, res) => {
  const nonce = crypto.randomUUID();
  res.setHeader("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
  res.send(content(nonce));
});

在每個請求上,伺服器生成一個新的 nonce,將其插入 CSP 和返回文件中的 <script> 標籤。請注意,伺服器

  • 為每個請求生成一個新的 nonce
  • 可以將 nonce 與外部和內聯指令碼一起使用
  • 對文件中的所有 <script> 標籤使用相同的 nonce

重要的是,伺服器使用某種模板來插入 nonce,而不是僅僅將它們插入到所有 <script> 標籤中:否則,伺服器可能會無意中將 nonce 插入到攻擊者注入的指令碼中。

請注意,nonce 只能用於具有 nonce 屬性的元素:即,只有 <script><style> 元素。

雜湊值

Fetch 指令還可以使用指令碼的雜湊值來保證其完整性。使用此方法,伺服器

  1. 使用雜湊函式(SHA-256、SHA-384 或 SHA-512 之一)計算指令碼內容的雜湊值
  2. 建立結果的Base64 編碼
  3. 附加一個標識所使用的雜湊演算法的字首(sha256-sha384-sha512- 之一)。

然後將其結果新增到指令中

http
Content-Security-Policy: script-src 'sha256-cd9827ad...'

當瀏覽器收到文件時,它會對指令碼進行雜湊處理,將結果與標題中的值進行比較,並且只有當它們匹配時才載入指令碼。

外部指令碼還必須包含 integrity 屬性才能使此方法生效。

這是一個 Express 程式碼片段,用於演示

js
const hash1 = "sha256-ex2O7MWOzfczthhKm6azheryNVoERSFrPrdvxRtP8DI=";
const hash2 = "sha256-H/eahVJiG1zBXPQyXX0V6oaxkfiBdmanvfG9eZWSuEc=";

const csp = `script-src '${hash1}' '${hash2}'`;
const content = `
  <script src="./main.js"></script>
  <script>console.log("hello!");</script>
    <h1>Hello world</h1> 
    `;

app.get("/", (req, res) => {
  res.setHeader("Content-Security-Policy", csp);
  res.send(content);
});

請注意:

  • 我們對文件中的每個指令碼都有一個單獨的雜湊值。
  • 對於外部指令碼“main.js”,我們還包括 integrity 屬性,並賦予它相同的值。
  • 與使用 nonce 的示例不同,CSP 和內容都可以是靜態的,因為雜湊值保持不變。這使得基於雜湊的策略更適合靜態頁面或依賴客戶端渲染的網站。

基於方案的策略

Fetch 指令可以列出一個方案,例如 https:,以允許使用該方案提供的資源。這例如允許策略要求所有資源載入都使用 HTTPS

http
Content-Security-Policy: default-src https:

基於位置的策略

Fetch 指令可以根據資源所在的位置控制資源載入。

關鍵字 'self' 允許與文件本身同源的資源

http
Content-Security-Policy: img-src 'self'

你還可以指定一個或多個主機名,可能包括萬用字元,並且只允許從這些主機提供的資源。例如,這可以用於允許從受信任的 CDN 提供內容。

http
Content-Security-Policy: img-src *.example.org

你可以指定多個位置。以下指令只允許與當前文件同源的影像,或從“example.org”的子域提供的影像,或從“example.com”提供的影像

http
Content-Security-Policy: img-src 'self' *.example.org  example.com

內聯 JavaScript

如果 CSP 包含 default-srcscript-src 指令,則除非採取額外措施啟用,否則不允許執行內聯 JavaScript。這包括

  • 頁面中 <script> 元素內包含的 JavaScript

    html
    <script>
      console.log("Hello from an inline script");
    </script>
    
  • 內聯事件處理程式屬性中的 JavaScript

    html
    <img src="x" onerror="console.log('Hello from an inline event handler')" />
    
  • javascript: URL 中的 JavaScript

    html
    <a href="javascript:console.log('Hello from a javascript: URL')"></a>
    

unsafe-inline 關鍵字可用於覆蓋此限制。例如,以下指令要求所有資源同源,但允許內聯 JavaScript

http
Content-Security-Policy: default-src 'self' 'unsafe-inline'

警告:開發人員應避免使用 'unsafe-inline',因為它會抵消 CSP 的大部分目的。內聯 JavaScript 是最常見的 XSS 向量之一,CSP 的最基本目標之一是防止其不受控制的使用。

如果內聯 <script> 元素受 nonce 或雜湊保護(如上所述),則允許使用它們。

如果指令包含 nonce 或雜湊表示式,則瀏覽器將忽略 unsafe-inline 關鍵字。

eval() 和類似 API

與內聯 JavaScript 類似,如果 CSP 包含 default-srcscript-src 指令,則不允許執行 eval() 和類似 API。這包括(除其他 API 外)

  • eval() 本身

    js
    eval('console.log("hello from eval()")');
    
  • Function() 建構函式

    js
    const sum = new Function("a", "b", "return a + b");
    
  • setTimeout()setInterval() 的字串引數

    js
    setTimeout("console.log('hello from setTimeout')", 1);
    

unsafe-eval 關鍵字可用於覆蓋此行為,並且與 unsafe-inline 一樣,出於相同的原因:開發人員應避免使用 unsafe-eval。有時很難刪除 eval() 的用法:在這種情況下,Trusted Types API 可以透過確保輸入符合定義的策略來使其更安全。

unsafe-inline 不同,unsafe-eval 關鍵字在包含 nonce 或雜湊表示式的指令中仍然有效。

嚴格 CSP

為了控制指令碼載入以緩解 XSS,推薦的做法是使用基於nonce雜湊的 fetch 指令。這稱為嚴格 CSP。與基於位置的 CSP(通常稱為允許列表 CSP)相比,這種型別的 CSP 具有兩個主要優勢

基於 nonce 的嚴格 CSP 如下所示

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

在此 CSP 中,我們

  • 使用 nonce 來控制允許載入哪些 JavaScript 資源
  • 阻止所有物件嵌入
  • 阻止所有使用 <base> 元素設定基本 URI 的情況。

基於雜湊的嚴格 CSP 相同,只是它使用雜湊而不是 nonce

http
Content-Security-Policy:
  script-src 'sha256-{HASHED_SCRIPT}';
  object-src 'none';
  base-uri 'none';

如果可以動態生成響應(包括內容本身),則基於 nonce 的指令更容易維護。否則,你需要使用基於雜湊的指令。基於雜湊的指令的問題在於,如果指令碼內容有任何更改,則需要重新計算並重新應用雜湊。

strict-dynamic 關鍵字

如上所述,當你使用不受你控制的指令碼時,嚴格 CSP 很難實現。如果第三方指令碼載入任何額外的指令碼,或使用任何內聯指令碼,則這將會失敗,因為第三方指令碼不會透過 nonce 或雜湊。

提供了 strict-dynamic 關鍵字來幫助解決此問題。它是一個可以包含在 fetch 指令中的關鍵字,其效果是,如果指令碼附加了 nonce 或雜湊,則該指令碼將允許載入本身沒有 nonce 或雜湊的其他指令碼。也就是說,nonce 或雜湊賦予指令碼的信任會傳遞給原始指令碼載入的指令碼(以及它們載入的指令碼等)。

例如,考慮一個如下所示的文件

html
<html lang="en-US">
  <head>
    <script
      src="./main.js"></script>
  </head>
  <body>
    <h1>Example page!</h1>
  </body>
</html>

它包含一個指令碼“main.js”,該指令碼建立並添加了另一個指令碼“main2.js”

js
console.log("hello");

const scriptElement = document.createElement("script");
scriptElement.src = `main2.js`;

document.head.appendChild(scriptElement);

我們使用如下 CSP 提供文件

http
Content-Security-Policy:
  script-src 'sha256-gEh1+8U9S1vkEuQSmmUMTZjyNSu5tIoECP4UXIEjMTk='

“main.js”指令碼將允許載入,因為其雜湊值與 CSP 中的值匹配。但其載入“main2.js”的嘗試將失敗。

如果我們將 'strict-dynamic' 新增到 CSP,則“main.js”將允許載入“main2.js”

http
Content-Security-Policy:
  script-src 'sha256-gEh1+8U9S1vkEuQSmmUMTZjyNSu5tIoECP4UXIEjMTk='
  'strict-dynamic'

'strict-dynamic' 關鍵字使得建立和維護基於 nonce 或雜湊的 CSP 變得更加容易,尤其是在網站使用第三方指令碼時。但是,它確實會降低 CSP 的安全性,因為如果你包含的指令碼根據潛在的 XSS 源建立 <script> 元素,則 CSP 將無法保護它們。

重構內聯 JavaScript 和 eval()

我們上面已經看到,CSP 預設不允許內聯 JavaScript。使用 nonce 或雜湊,開發人員可以使用內聯 <script> 標籤,但你仍然需要重構程式碼以刪除其他不允許的模式,包括內聯事件處理程式、javascript: URL 和 eval() 的用法。例如,內聯事件處理程式通常應替換為對 addEventListener() 的呼叫

html
<p onclick="console.log('Hello from an inline event handler')">click me</p>
html
<!-- served with the following CSP:
 `script-src 'sha256-AjYfua7yQhrSlg807yyeaggxQ7rP9Lu0Odz7MZv8cL0='`
 -->
<p id="hello">click me</p>
<script>
  const hello = document.querySelector("#hello");
  hello.addEventListener("click", () => {
    console.log("Hello from an inline script");
  });
</script>

點選劫持保護

frame-ancestors 指令可用於控制允許哪些文件(如果有)將此文件嵌入到巢狀瀏覽上下文(例如 <iframe>)中。這是一種有效的點選劫持攻擊防禦措施,因為這些攻擊依賴於將目標網站嵌入到攻擊者控制的網站中。

frame-ancestors 的語法是 fetch 指令語法的子集:你可以提供單個關鍵字值 'none' 或一個或多個源表示式。但是,你唯一可以使用的源表示式是方案、主機名或 'self' 關鍵字值。

除非你需要網站可嵌入,否則應將 frame-ancestors 設定為 'none'

http
Content-Security-Policy: frame-ancestors 'none'

此指令是 X-Frame-Options 標頭更靈活的替代方案。

升級不安全請求

強烈建議網站開發人員透過 HTTPS 提供所有內容。在將網站升級到 HTTPS 的過程中,網站有時會透過 HTTPS 提供主文件,但透過 HTTP 提供其資源,例如,使用如下標記

html
<script src="http://example.org/my-cat.js"></script>

這稱為混合內容,不安全資源的存在極大地削弱了 HTTPS 提供的保護。根據瀏覽器實現的混合內容演算法,如果文件透過 HTTPS 提供,則不安全資源分為“可升級內容”和“可阻止內容”。可升級內容升級到 HTTPS,可阻止內容被阻止,可能導致頁面中斷。

混合內容的最終解決方案是開發人員透過 HTTPS 載入所有資源。然而,即使網站實際上能夠透過 HTTPS 提供所有內容,開發人員重寫網站用於載入所有資源的 URL 仍然可能非常困難(甚至在涉及存檔內容時實際上不可能)。

upgrade-insecure-requests 指令旨在解決此問題。此指令沒有任何值:要設定它,只需包含指令名稱

http
Content-Security-Policy: upgrade-insecure-requests

如果文件上設定了此指令,則瀏覽器將在以下情況下自動將任何 HTTP URL 升級到 HTTPS

  • 載入資源的請求(例如影像、指令碼或字型)
  • 與文件同源的導航請求(例如連結目標)
  • 巢狀瀏覽上下文中的導航請求,例如 iframes
  • 表單提交

但是,目標是不同來源的頂級導航請求不會升級。

例如,假設 https://example.org 的文件使用包含 upgrade-insecure-requests 指令的 CSP 提供,並且文件包含如下標記

html
<script src="http://example.org/my-cat.js"></script>
<script src="http://not-example.org/another-cat.js"></script>

瀏覽器將自動將這兩個請求升級到 HTTPS。

假設文件還包含以下內容

html
<a href="http://example.org/more-cats">See some more cats!</a>
<a href="http://not-example.org/even-more-cats">More cats, on another site!</a>

瀏覽器將第一個連結升級到 HTTPS,但不會升級第二個連結,因為它導航到不同的來源。

此指令不能替代 Strict-Transport-Security 標頭(也稱為 HSTS),因為它不會將外部連結升級到網站。網站應包含此指令和 Strict-Transport-Security 標頭。

測試你的策略

為了簡化部署,CSP 可以以僅報告模式部署。策略不強制執行,但任何違規都會發送到策略中指定的報告端點。此外,僅報告標頭可用於測試策略的未來修訂版,而無需實際部署它。

你可以使用 Content-Security-Policy-Report-Only HTTP 標頭來指定你的策略,如下所示

http
Content-Security-Policy-Report-Only: policy

如果同一個響應中同時存在 Content-Security-Policy-Report-Only 標頭和 Content-Security-Policy 標頭,則兩個策略都將生效。Content-Security-Policy 標頭中指定的策略將強制執行,而 Content-Security-Policy-Report-Only 策略會生成報告但不強制執行。

請注意,與普通內容安全策略不同,僅報告策略不能在 <meta> 元素中提供。

違規報告

報告 CSP 違規的推薦方法是使用 報告 API,在 Reporting-Endpoints 中宣告端點,並使用 Content-Security-Policy 標頭的 report-to 指令將其中一個指定為 CSP 報告目標。

警告:你還可以使用 CSP report-uri 指令來指定 CSP 違規報告的目標 URL。這透過 POST 操作傳送稍微不同的 JSON 報告格式,其中 Content-Typeapplication/csp-report。此方法已棄用,但在所有瀏覽器都支援 report-to 之前,你應該同時宣告這兩個。有關此方法的更多資訊,請參閱 report-uri 主題。

伺服器可以使用 Reporting-Endpoints HTTP 響應頭通知客戶端在哪裡傳送報告。此標頭將一個或多個端點 URL 定義為逗號分隔列表。例如,要定義名為 csp-endpoint 的報告端點,該端點在 https://example.com/csp-reports 接受報告,伺服器的響應頭可能如下所示

http
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"

如果你想擁有處理不同型別報告的多個端點,你可以這樣指定它們

http
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports",
                     hpkp-endpoint="https://example.com/hpkp-reports"

然後,你可以使用 Content-Security-Policy 標頭的 report-to 指令來指定應使用特定的已定義端點進行報告。例如,要將 CSP 違規報告發送到 https://example.com/csp-reports 用於 default-src,你可能會發送如下所示的響應頭

http
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
Content-Security-Policy: default-src 'self'; report-to csp-endpoint

當 CSP 違規發生時,瀏覽器透過 HTTP POST 操作將報告作為 JSON 物件傳送到指定端點,其中 Content-Typeapplication/reports+json。報告是 Report 物件的序列化形式,包含一個 type 屬性,其值為 "csp-violation",以及一個作為 CSPViolationReportBody 物件的序列化形式的 body

典型的物件可能如下所示

json
{
  "age": 53531,
  "body": {
    "blockedURL": "inline",
    "columnNumber": 39,
    "disposition": "enforce",
    "documentURL": "https://example.com/csp-report",
    "effectiveDirective": "script-src-elem",
    "lineNumber": 121,
    "originalPolicy": "default-src 'self'; report-to csp-endpoint-name",
    "referrer": "https://www.google.com/",
    "sample": "console.log(\"lo\")",
    "sourceFile": "https://example.com/csp-report",
    "statusCode": 200
  },
  "type": "csp-violation",
  "url": "https://example.com/csp-report",
  "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"
}

你需要設定一個伺服器來接收具有給定 JSON 格式和內容型別的報告。處理這些請求的伺服器然後可以以最適合你需求的方式儲存或處理傳入的報告。

另見