內容安全政策

Mike West
喬梅利
Joe Medley

網路的安全性模型是以相同來源政策為基礎。https://mybank.com 的程式碼只能存取 https://mybank.com 的資料,而 https://evil.example.com 絕對不應允許存取。每個來源都會與網路的其他部分隔離,為開發人員提供用於建立及玩遊戲的安全沙箱。理論上來說,這真是太棒了。在實務上,攻擊者發現了顛覆系統的方法。

跨網站指令碼攻擊 (XSS) 攻擊,例如誘騙網站傳送惡意程式碼和預期內容,藉此略過相同的來源政策。這會造成重大問題,因為瀏覽器信任網頁上顯示的所有程式碼,是該網頁安全性來源的正當部分。XSS 一覽表是老舊、具代表性的交叉方法,攻擊者可能會利用這些方法,插入惡意程式碼來違反這項信任。如果攻擊者成功插入「任何」程式碼,基本上就是遊戲過程太多:使用者工作階段資料會遭駭,而應保存的資訊也會竊取至「壞蛋」中。可以的話,我們顯然會希望避免發生這種情況。

本總覽重點說明一種防禦機制,可大幅降低現代瀏覽器中 XSS 攻擊的風險和影響:內容安全政策 (CSP)。

重點摘要

  • 使用許可清單告知客戶允許和禁止的內容。
  • 瞭解可用的指令。
  • 瞭解這些關鍵字採用的關鍵字。
  • 內嵌程式碼和 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 指令,可限制能夠以工作站、共用工作站或 Service 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' 允許由文字轉 JavaScript 機制,例如 eval。(我們也將說明)

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

沙箱機制

還有一個指令值得討論:sandbox。跟我們看過的其他例子稍有不同,因為這項政策會限制網頁可執行的動作,而不僅限於頁面可載入的資源。如有 sandbox 指令,系統會將網頁視為該網頁在具有 sandbox 屬性的 <iframe> 內載入。這可能會對頁面造成多種影響:強制將頁面強制設為不重複來源,以及防止表單提交等等。這實際上比本文的範圍更廣,但您可以在 HTML5 規格的「沙箱」一節中找到有效沙箱屬性的完整資訊。

中繼標記

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>

現在,將 Nonce 加入 script-src 指令中,加至 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 以上版本,可以開啟開發人員工具,然後重新載入頁面。「Console」分頁會顯示錯誤訊息,每個內嵌指令碼都有正確的 sha256 雜湊。

一併評估

即使攻擊者無法直接插入指令碼,他們也可能誘騙應用程式將文字以其他方式轉換為可執行的 JavaScript,並代替他們執行。eval()、新的 函式()、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 指令便是很好的範例。

然而,更好的選擇會是提供預編譯的範本語言 (例如 Handlebar 會)。預先編譯範本也能加快使用者體驗,速度比最快的執行階段實作更快,而且也更加安全。如果 eval 和其文字轉 JavaScript 暴露對應用程式的必要元素,您可以在 script-src 指令中加入 'unsafe-eval' 做為允許來源,但我們強烈建議您不要這樣做。禁止執行字串的功能,讓攻擊者更難在您的網站上執行未經授權的程式碼。

報表

CSP 能夠封鎖不受信任的資源,可為使用者提供巨大的益處,但將一些通知傳回伺服器讓您可以找出並消除任何允許惡意植入的錯誤。為此,您可以指示瀏覽器將 JSON 格式的違規報告 POSTreport-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 標頭,而是傳送 Content-Security-Policy-Report-Only 標頭。

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 級別 1 為基礎的政策,必須將該政策變更為 child-src。CSP 層級 3 已不再需要此功能。

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

  • Twitter 的 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@ 郵寄清單封存檔,或自行加入。

意見回饋