內容安全政策

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,以及從目前網頁的來源執行。

控制台錯誤:拒絕載入指令碼 '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 標記的內容移至外部檔案,並將 javascript: 網址和 <a ... onclick="[JAVASCRIPT]"> 替換為適當的 addEventListener() 呼叫。舉例來說,您可以將下列內容改寫為:

<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 第 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 的 Web 應用程式安全性工作團隊已經開始在規格的下個疊代作業:內容安全政策等級 3 上執行。

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

意見回饋