內容安全政策

Mike West
Joe Medley
Joe Medley

網路的安全性模型源自同源政策。來自 https://mybank.com 的程式碼應只可存取 https://mybank.com 的資料,而 https://evil.example.com 絕對不應允許存取。每個來源都會與其他網站保持隔離,讓開發人員在安全的沙箱中進行建構和測試。理論上,這麼做非常聰明。實際上,攻擊者已找到巧妙的方法來破壞系統。

舉例來說,跨網站指令碼攻擊 (XSS) 會誘騙網站在傳送預期內容時一併傳送惡意程式碼,藉此規避相同來源政策。這會造成嚴重問題,因為瀏覽器會將網頁上顯示的所有程式碼視為該網頁安全來源的合法部分。XSS 速查表是舊但代表性的橫斷面,列出攻擊者可能用來透過注入惡意程式碼侵犯信任的各種方法。如果攻擊者成功插入「任何」程式碼,遊戲就會結束:使用者工作階段資料遭到入侵,而應保密的資訊會外流至惡意人士手中。我們當然會盡量避免這種情況。

這份總覽將介紹一種防禦機制,可大幅降低 XSS 攻擊在現代瀏覽器中造成的風險和影響:內容安全政策 (CSP)。

TL;DR

  • 使用許可清單告知用戶端哪些內容允許,哪些內容不允許。
  • 瞭解可用的指令。
  • 瞭解他們採用的關鍵字。
  • 內嵌程式碼和 eval() 都會被視為有害。
  • 請先向伺服器回報違規行為,再執行違規處置。

來源允許清單

XSS 攻擊會利用瀏覽器無法區分應用程式所屬指令碼,與第三方惡意插入的指令碼之間的差異。舉例來說,這個頁面底部的 Google +1 按鈕會在這個頁面來源的內容中載入並執行 https://apis.google.com/js/plusone.js 的程式碼。我們信任該程式碼,但我們無法預期瀏覽器會自行判斷 apis.google.com 的程式碼很棒,而 apis.evil.example.com 的程式碼可能不太好。瀏覽器會下載並執行任何網頁要求的程式碼,不論來源為何。

CSP 不會全盤接受伺服器提供的「所有內容」,而是透過定義 Content-Security-Policy HTTP 標頭的方式,讓您針對信任的內容來源建立許可清單,並指示瀏覽器只執行或顯示這些來源的資源。即使攻擊者找到可用來注入指令碼的漏洞,該指令碼也不會符合許可清單,因此不會執行。

由於我們信任 apis.google.com 會提供有效的程式碼,而且我們也相信自己會這麼做,因此我們來定義一項政策,只允許來自這兩個來源的腳本執行:

Content-Security-Policy: script-src 'self' https://apis.google.com

很簡單,對吧?如您所知,script-src 是指令,可控制特定網頁的一系列指令碼相關權限。我們已將 'self' 指定為一個有效的指令碼來源,並將 https://apis.google.com 指定為另一個來源。瀏覽器會盡責地透過 HTTPS 從 apis.google.com 下載及執行 JavaScript,以及從目前網頁的來源下載及執行 JavaScript。

控制台錯誤:拒絕載入指令碼「http://evil.example.com/evil.js」,因為該指令碼違反下列內容安全政策指令:script-src 'self' https://apis.google.com

定義這項政策後,瀏覽器就會直接擲回錯誤,而不會從任何其他來源載入指令碼。當聰明的攻擊者成功在您的網站上植入程式碼時,他們會直接看到錯誤訊息,而非預期的成功畫面。

政策適用於多種資源

雖然指令碼資源是最明顯的安全性風險,但 CSP 提供豐富的政策指示集,可針對允許網頁載入的資源進行精細控管。您已看過 script-src,因此應該能清楚瞭解這個概念。

讓我們快速瀏覽其他資源指令。以下清單代表第 2 級指令的狀態。第 3 級規格已發布,但在主要瀏覽器中幾乎未實作

  • base-uri 會限制網頁 <base> 元素中可顯示的網址。
  • child-src 會列出 worker 和嵌入式框架內容的網址。舉例來說,child-src https://youtube.com 會允許嵌入 YouTube 影片,但不允許嵌入其他來源的影片。
  • connect-src 會限制您可以連線的來源 (透過 XHR、WebSocket 和 EventSource)。
  • font-src 可指定可提供網路字型的來源。您可以透過 font-src https://themes.googleusercontent.com 啟用 Google 的網路字型。
  • form-action 會列出可從 <form> 標記提交的有效端點。
  • frame-ancestors 可指定可嵌入目前網頁的來源。這個指令適用於 <frame><iframe><embed><applet> 標記。這個指示無法用於 <meta> 標記,且只適用於非 HTML 資源。
  • frame-src 在第 2 級淘汰,但在第 3 級恢復。如果未提供,則會改回使用 child-src
  • img-src 會定義可載入圖片的來源。
  • media-src 會限制允許提供影片和音訊的來源。
  • object-src 可控制 Flash 和其他外掛程式。
  • plugin-types 會限制網頁可叫用的外掛程式類型。
  • report-uri 會指定網址,當瀏覽器偵測到違反內容安全性政策時,就會傳送報告。這個指令無法用於 <meta> 標記。
  • style-srcscript-src 的樣式表對應項目。
  • upgrade-insecure-requests 會指示使用者代理程式重新編寫網址配置,將 HTTP 變更為 HTTPS。這個指令適用於需要重寫大量舊網址的網站。
  • worker-src 是 CSP 3 級指令,可限制可做為 worker、共用 worker 或服務 worker 載入的網址。截至 2017 年 7 月,此指令的實作項目有限

根據預設,指令會全面開放。如果您未為指令 (例如 font-src) 設定特定政策,則該指令的預設行為會視同您已將 * 指定為有效來源 (例如,您可以從任何地方載入字型,不受限制)。

您可以指定 default-src 指示詞來覆寫預設行為。這個指令會為您未指定的大部分指令定義預設值。一般來說,這項設定適用於結尾為 -src 的任何指令。如果 default-src 設為 https://example.com,但您無法指定 font-src 指令,則只能從 https://example.com 載入字型,而無法從其他位置載入。在先前的範例中,我們只指定 script-src,這表示圖片、字型等資源可以從任何來源載入。

以下指令不會使用 default-src 做為備用值。請注意,如果未設定這些值,就會允許任何內容。

  • base-uri
  • form-action
  • frame-ancestors
  • plugin-types
  • report-uri
  • sandbox

您可以根據特定應用程式的需要,使用這些指令的數量,只要在 HTTP 標頭中列出每個指令,並以分號分隔即可。請務必在單一指令中列出特定類型的所有必要資源。如果您寫入 script-src https://host1.com; script-src https://host2.com 之類的內容,系統會直接忽略第二個指令。以下內容會正確指定兩個來源皆有效:

script-src https://host1.com https://host2.com

舉例來說,如果您有應用程式從內容傳遞網路 (例如 https://cdn.example.net) 載入所有資源,且知道不需要任何框架內容或外掛程式,則政策可能會像以下所示:

Content-Security-Policy: default-src https://cdn.example.net; child-src 'none'; object-src 'none'

實作詳情

您會在網路上的各種教學課程中看到 X-WebKit-CSPX-Content-Security-Policy 標頭。往後,您應忽略這些前置標頭。現代瀏覽器 (IE 除外) 支援未加上前置字元的 Content-Security-Policy 標頭。這是你應該使用的標頭。

無論您使用哪個標頭,政策都是依網頁逐一定義:您必須在每個要確保受到保護的回應中一併傳送 HTTP 標頭。這樣一來,您就能根據特定需求,針對特定網頁微調政策,享有極大的彈性。網站中可能有一組網頁有 +1 按鈕,而其他網頁則沒有:您可以只在必要時載入按鈕程式碼。

每個指令中的來源清單皆可彈性調整。您可以依據通訊協定 (data:https:) 指定來源,也可以指定從僅限主機名稱 (example.com,可比對該主機上的任何來源:任何通訊協定、任何通訊埠) 到完整的 URI (https://example.com:443,僅比對 HTTPS、example.com 和 443 通訊埠) 的範圍。系統會接受萬用字元,但只能用於網址配置、通訊埠或主機名稱的最左位置:*://*.example.com:* 會比對 example.com 的所有子網域 (但比對 example.com 本身),使用任何網址配置和任何通訊埠。

來源清單也接受四個關鍵字:

  • 'none' 沒有任何相符項目,這點您應該也能理解。
  • 'self' 會比對目前的來源,但不會比對其子網域。
  • 'unsafe-inline' 允許內嵌 JavaScript 和 CSS。(稍後我們會進一步說明這點)。
  • 'unsafe-eval' 允許 eval 等文字轉 JavaScript 機制。(我們也會討論這個問題)。

這些關鍵字必須加上單引號。舉例來說,script-src 'self' (含引號) 會授權執行目前主機的 JavaScript;script-src self (不含引號) 會允許執行名為「self」的伺服器 (而非目前主機) 的 JavaScript,這可能不是您想要的結果。

沙箱機制

還有一個值得討論的指令:sandbox。這與我們先前討論的其他內容略有不同,因為它會限制網頁可採取的動作,而非限制網頁可載入的資源。如果有 sandbox 指令,系統會將網頁視為在具有 sandbox 屬性的 <iframe> 內載入。這可能會對網頁產生多種影響,包括強制將網頁設為單一來源,以及防止表單提交等。這部分稍微超出本文的範圍,但您可以在 HTML5 規格中的「Sandboxing」部分中,找到有效的沙箱屬性詳細資訊。

中繼標記

CSP 偏好的提交機制是 HTTP 標頭。不過,直接在標記中設定網頁政策可能會很有用。請使用含有 http-equiv 屬性的 <meta> 標記執行這項操作:

<meta
  http-equiv="Content-Security-Policy"
  content="default-src https://cdn.example.net; child-src 'none'; object-src 'none'"
/>

這無法用於 frame-ancestorsreport-urisandbox

內嵌程式碼經認定有害

請注意,CSP 是以許可清單來源為基礎,這是明確的指示方式,可指示瀏覽器將特定資源組視為可接受,並拒絕其餘資源。不過,以來源為準的許可清單無法解決 XSS 攻擊所帶來的最大威脅:內嵌指令碼注入。如果攻擊者可以插入直接包含某些惡意酬載 (<script>sendMyDataToEvilDotCom();</script>) 的指令碼標記,瀏覽器就沒有機制可將其與合法的內嵌指令碼標記區分開。CSP 會完全禁止內嵌指令碼,藉此解決這個問題。這是確保安全性的唯一方法。

這項禁令不僅禁止直接嵌入 script 標記的程式碼,也禁止內嵌事件處理常式和 javascript: 網址。您必須將 script 標記的內容移至外部檔案,並使用適當的 addEventListener() 呼叫取代 javascript: 網址和 <a ... onclick="[JAVASCRIPT]">。舉例來說,您可以將下列內容改寫為:

<script>
  function doAmazingThings() {
    alert('YOU AM AMAZING!');
  }
</script>
<button onclick="doAmazingThings();">Am I amazing?</button>

改為類似以下的內容:

<!-- amazing.html -->
<script src="amazing.js"></script>
<button id="amazing">Am I amazing?</button>

<div style="clear:both;"></div>
// amazing.js
function doAmazingThings() {
  alert('YOU AM AMAZING!');
}
document.addEventListener('DOMContentLoaded', function () {
  document.getElementById('amazing').addEventListener('click', doAmazingThings);
});

除了與 CSP 搭配良好運作之外,重新撰寫的程式碼還有許多優點;無論您是否使用 CSP,這都是最佳做法。內嵌式 JavaScript 會將結構和行為混合在一起,這正是您不該採用的做法。外部資源更容易讓瀏覽器快取,開發人員也更容易瞭解,且有助於編譯和精簡。將程式碼移至外部資源,就能編寫出更優質的程式碼。

內嵌樣式會以相同方式處理:style 屬性和 style 標記都應整合至外部樣式表,以防範 CSS 啟用的各種出乎意料的聰明資料擷取方法。

如果您必須使用內嵌指令碼和樣式,可以在 script-srcstyle-src 指令中新增 'unsafe-inline' 做為允許的來源,啟用這項功能。您也可以使用 Nonce 或雜湊 (請參閱下方說明),但不建議這麼做。禁止內嵌指令碼是 CSP 提供的最大安全性勝利,而禁止內嵌樣式同樣可強化應用程式。您必須先花點功夫,確保所有程式碼移至離線後能正常運作,但這項權衡是值得的。

如果您必須使用

CSP 2 級可為內嵌指令碼提供回溯相容性,讓您使用加密 Nonce (使用一次的數字) 或雜湊,將特定內嵌指令碼加入許可清單。雖然這麼做可能很麻煩,但在緊要關頭時很實用。

如要使用 Nonce,請為指令碼標記提供 Nonce 屬性。其值必須與信任來源清單中的值相符。例如:

<script nonce="EDNnf03nceIOfn39fn3e9h3sdfa">
  // Some inline code I can't remove yet, but need to asap.
</script>

接著,請在 script-src 指示詞中加入 Nonce,並附加至 nonce- 關鍵字。

Content-Security-Policy: script-src 'nonce-EDNnf03nceIOfn39fn3e9h3sdfa'

請注意,每個網頁要求都必須重新產生 Nonce,且 Nonce 不得可猜測。

雜湊值的運作方式也大致相同。請不要在指令碼標記中新增程式碼,而是建立指令碼本身的 SHA 雜湊,並將其新增至 script-src 指示。舉例來說,假設您的網頁包含以下內容:

<script>
  alert('Hello, world.');
</script>

您的政策應包含以下內容:

Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng='

這裡有幾點要注意。sha*- 前置字元會指定產生雜湊的演算法。在上述範例中,使用的是 sha256-。CSP 也支援 sha384-sha512-。產生雜湊時,請勿加入 <script> 標記。大小寫和空格也相當重要,包括前置或後置空格。

只要在 Google 搜尋「產生 SHA 雜湊值」相關資訊,就能找到多種語言的解決方案。使用 Chrome 40 以上版本時,您可以開啟開發人員工具,然後重新載入網頁。控制台分頁會顯示錯誤訊息,並附上每個內嵌指令碼的正確 sha256 雜湊值。

也要評估

即使攻擊者無法直接插入指令碼,仍可能誘騙應用程式將原本不具活性的文字轉換為可執行的 JavaScript,並代為執行。eval()、new Function()、setTimeout([string], ...)setInterval([string], ...) 都是向量,經過這些向量後,插入的文字可能會執行意料之外的惡意內容。CSP 對這項風險的預設回應是完全封鎖所有這些載體。

這會對應用程式建構方式造成多重影響:

  • 您必須透過內建的 JSON.parse 剖析 JSON,而非依賴 eval自 IE8 以來,每個瀏覽器都支援原生 JSON 作業,而且完全安全。
  • 請使用內嵌函式,而非字串,重新編寫您目前正在進行的任何 setTimeoutsetInterval 呼叫。例如:
setTimeout("document.querySelector('a').style.display = 'none';", 10);

建議改為以下寫法:

setTimeout(function () {
  document.querySelector('a').style.display = 'none';
}, 10);
  • 避免在執行階段使用內嵌範本:許多範本庫會大量使用 new Function(),以便在執行階段加快範本產生的速度。這是動態程式設計的實用應用程式,但可能會評估惡意文字。部分架構會預設支援 CSP,並在沒有 eval 時改用強大的剖析器。AngularJS 的 ng-csp 指令就是這方面的好例子。

不過,您可以選擇使用提供預先編譯功能的模板語言 (例如 Handlebars)。預先編譯範本可讓使用者體驗更快,甚至比最快的執行階段實作更快,而且更安全。如果 eval 和其文字轉 JavaScript 的兄弟皆對應用程式至關重要,您可以在 script-src 指示中將 'unsafe-eval' 新增為允許的來源,藉此啟用這兩者,但我們強烈不建議這麼做。禁止執行字串的功能,可讓攻擊者更難在您的網站上執行未經授權的程式碼。

報表

CSP 在用戶端封鎖不受信任的資源的能力,對使用者來說是個重大勝利,但如果能將某種通知傳回伺服器,讓您能找出並修正任何允許惡意注入的錯誤,將會非常有幫助。為此,您可以指示瀏覽器將 POST JSON 格式的違規報告傳送至 report-uri 指令中指定的位置。

Content-Security-Policy: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;

這些報表會如下所示:

{
  "csp-report": {
    "document-uri": "http://example.org/page.html",
    "referrer": "http://evil.example.com/",
    "blocked-uri": "http://evil.example.com/evil.js",
    "violated-directive": "script-src 'self' https://apis.google.com",
    "original-policy": "script-src 'self' https://apis.google.com; report-uri http://example.org/my_amazing_csp_report_parser"
  }
}

這項資訊包含大量資訊,可協助您追查違規的具體原因,包括發生違規的網頁 (document-uri)、該網頁的參照來源 (請注意,與 HTTP 標頭欄位不同,這個索引鍵「不會」拼錯)、違反網頁政策的資源 (blocked-uri)、違規的具體指令 (violated-directive),以及網頁的完整政策 (original-policy)。

僅限報表

如果您剛開始使用 CSP,建議您先評估應用程式的目前狀態,再向使用者推出嚴格的政策。在完成部署前,您可以要求瀏覽器監控政策,回報違規情形,但不強制執行限制。請改為傳送 Content-Security-Policy-Report-Only 標頭,而非 Content-Security-Policy 標頭。

Content-Security-Policy-Report-Only: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;

在僅回報模式中指定的政策不會封鎖受限制的資源,但會將違規報告傳送至您指定的位置。您甚至可以傳送兩個標頭,在執行一項政策的同時監控另一項政策。這是評估應用程式 CSP 變更效果的絕佳方法:為新政策啟用報表功能、監控違規報告,並修正任何出現的錯誤;當您對效果感到滿意時,即可開始強制執行新政策。

實際使用

CSP 1 在 Chrome、Safari 和 Firefox 中相當實用,但在 IE 10 中僅提供有限的支援。您可以前往 caniuse.com 查看詳細資訊。Chrome 40 以上版本已支援 CSP 2 級。Twitter 和 Facebook 等大型網站已部署了標頭 (Twitter 的個案研究值得一讀),而且標準也已準備就緒,您可以開始在自己的網站上部署。

為應用程式建立政策的第一步,是評估您實際載入的資源。當您認為自己已掌握應用程式中各項元素的組合方式後,請根據這些規定設定政策。我們將逐步說明幾種常見用途,並決定如何在 CSP 的保護範圍內提供最佳支援。

應用實例 #1:社群媒體小工具

  • Google 的 +1 按鈕包含來自 https://apis.google.com 的指令碼,並嵌入來自 https://plusone.google.com<iframe>。您需要建立包含這兩個來源的政策,才能嵌入按鈕。最小政策為 script-src https://apis.google.com; child-src https://plusone.google.com。您也必須確保 Google 提供的 JavaScript 程式碼片段會提取至外部 JavaScript 檔案。如果您有使用 frame-src 的 Level 1 政策,Level 2 會要求您將其變更為 child-src。在 CSP 層級 3 中,這項做法已不再必要。

  • Facebook 的按讚按鈕有許多實作選項。建議您繼續使用 <iframe> 版本,因為這個版本會與網站的其他部分安全地隔離。需要 child-src https://facebook.com 指令才能正常運作。請注意,根據預設,Facebook 提供的 <iframe> 程式碼會載入相對網址 //facebook.com。請將其變更為明確指定 HTTPS:https://facebook.com。除非必要,否則沒有理由使用 HTTP。

  • Twitter 的「推文按鈕」需要存取指令碼和框架,兩者都託管在 https://platform.twitter.com 中。(Twitter 同樣會預設提供相對網址;在本機複製/貼上程式碼時,請編輯程式碼以指定 HTTPS)。只要將 Twitter 提供的 JavaScript 程式碼片段移至外部 JavaScript 檔案,就能使用 script-src https://platform.twitter.com; child-src https://platform.twitter.com

  • 其他平台也有類似的要求,可以以類似方式處理。建議您只設定 default-src'none',並觀察控制台,判斷需要啟用哪些資源才能讓小工具運作。

加入多個小工具很簡單:只要合併政策指示,並記得將單一類型的所有資源合併為單一指示即可。如果您想要使用所有三個社群媒體小工具,政策會如下所示:

script-src https://apis.google.com https://platform.twitter.com; child-src https://plusone.google.com https://facebook.com https://platform.twitter.com

應用實例 2:封鎖

假設您經營銀行網站,並想確保只有您自行編寫的資源才能載入。在這種情況下,請先設定預設政策,一律封鎖所有內容 (default-src 'none'),然後再從中調整。

假設銀行在 https://cdn.mybank.net 載入 CDN 的所有圖片、樣式和指令碼,並透過 XHR 連線至 https://api.mybank.com/,以便下載各種資料位元。使用框架,但僅限於網站的本機頁面 (不含第三方來源)。網站上沒有 Flash、字型或其他額外內容。我們可以傳送的最嚴格 CSP 標頭如下:

Content-Security-Policy: default-src 'none'; script-src https://cdn.mybank.net; style-src https://cdn.mybank.net; img-src https://cdn.mybank.net; connect-src https://api.mybank.com; child-src 'self'

應用實例 3:僅限 SSL

婚戒討論論壇的管理員希望確保所有資源只透過安全通道載入,但他並未編寫太多程式碼;重新撰寫大量第三方論壇軟體,其中充斥著內嵌指令碼和樣式,這超出他的能力範圍。以下政策將生效:

Content-Security-Policy: default-src https:; script-src https: 'unsafe-inline'; style-src https: 'unsafe-inline'

即使在 default-src 中指定 https:,指令碼和樣式指示詞也不會自動繼承該來源。每個指令都會完全覆寫特定資源類型的預設值。

未來

內容安全政策第 2 級為 候選最佳化建議。W3C 的網路應用程式安全性工作小組已開始著手處理規格書的下一個版本,也就是內容安全性政策第 3 級

如果您對這些即將推出的功能有興趣,可以瀏覽 public-webappsec@ 的郵件討論串留檔案,或親自加入討論。

意見回饋