內容安全政策

Mike West
Joe Medley
Joe Medley

網路的安全性模型是以相同來源政策為基礎。https://mybank.com 的程式碼只能存取 https://mybank.com 的資料,且 https://evil.example.com 絕對不得存取。每個來源都會與網路的其他部分區隔開來,讓開發人員能在安全的情況下建構及暢玩遊戲。理論上來說,這超棒的實際上,攻擊者找到了巧妙的方法破壞系統。

跨網站指令碼 (XSS) 攻擊舉例來說,誘騙網站傳送惡意程式碼與預定內容,藉此略過同一項來源政策。這個問題相當嚴重,因為瀏覽器信任頁面上顯示的所有程式碼,都屬於該網頁的安全性來源。XSS 一覽表是一組舊式但代表性的做法,攻擊者可能利用這些方法植入惡意程式碼,藉此違反此信任關係。如果攻擊者成功插入「任何」程式碼,基本上就差了一點:使用者工作階段資料會遭到破壞,並且應妥善保管的資訊會洩露給 The Bad Guys。我們必須盡可能避免這種情況。

本總覽的重點是,可以大幅降低新型瀏覽器 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、WebSockets 和 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-src是樣式表的 script-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' 允許 eval 等文字轉 JavaScript 機制。(我們也會探討此部分)

這些關鍵字需要單引號。舉例來說,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 附加至 nonce- 關鍵字。script-src

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

請注意,每次網頁要求都必須重新產生 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()、new Function()、setTimeout([string], ...)setInterval([string], ...) 都是向量,這些插入的文字最終可能會執行意想不到的惡意內容。CSP 對這個風險的預設回應會完全封鎖這些向量。

這對建構應用程式的方式的影響不只部分:

  • 您必須透過內建的 JSON.parse (而非 eval) 剖析 JSON。原生 JSON 作業自 IE8 起在所有瀏覽器中都能使用,而且完全安全無虞。
  • 改寫使用內嵌函式 (而非字串) 進行的任何 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 和 Text-to-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

  • 其他平台的規定類似,也能以類似方式處理。建議您只設定 'none'default-src,然後查看主控台,判斷需要啟用哪些資源,小工具才能正常運作。

提供多個小工具的方式相當簡單明瞭:只要結合政策指令,請記得將單一類型的所有資源合併成單一指令。如要使用這三個社群媒體小工具,政策看起來會像這樣:

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-websec@ 郵寄清單封存檔,或親自加入。

意見回饋: