內容安全策略(CSP),防禦 XSS 攻擊的好助手

Nicolas Hoffmann發表於2016-10-10

很久之前,我的個人網站被攻擊了。我不知道它是如何發生的,但它確實發生了。幸運的是,攻擊帶來的破壞是很小的:一小段 JavaScript 被注入到了某些頁面的底部。我更新了 FTP 和其它的口令,清理了一些檔案,事情就這樣結束了。

有一點使我很惱火:在當時,還沒有一種簡便的方案能夠使我知道那裡有問題,更重要的是能夠保護網站的訪客不被這段惱人的程式碼所擾。

現在有一種方案出現了,這種技術在上述兩方面都十分的成功。它就是內容安全策略content security policy(CSP)。

什麼是 CSP?

其核心思想十分簡單:網站通過傳送一個 CSP 頭部,來告訴瀏覽器什麼是被授權執行的與什麼是需要被禁止的。

這裡有一個 PHP 的例子:

<?php
    header("Content-Security-Policy: <your directives>");
?>

一些指令

你可以定義一些全域性規則或者定義一些涉及某一類資源的規則:

default-src 'self' ;
     # self = 同埠,同域名,同協議 => 允許

基礎引數是 default-src:如果沒有為某一類資源設定指令規則,那麼瀏覽器就會使用這個預設引數值。

script-src 'self' www.google-analytics.com ;
     # 來自這些域名的 JS 檔案 => 允許

在這個例子中,我們已經授權了 www.google-analytics.com 這個域名來源的 JavaScript 檔案使用到我們的網站上。我們也新增了 'self' 這個關鍵詞;如果我們通過 script-src 來重新設定其它的規則指令,它將會覆蓋 default-src 規則。

如果沒有指明協議(scheme)或埠,它就會強制選擇與當前頁面相同的協議或埠。這樣做防止了混合內容(LCTT 譯註:混合內容指 HTTPS 頁面中也有非 HTTPS 資源,可參見: https://developer.mozilla.org/zh-CN/docs/Security/MixedContent )。如果頁面是 https://example.com,那麼你將無法載入 http://www.google-analytics.com/file.js 因為它已經被禁止了(協議不匹配)。然而,有一個例外就是協議的提升是被允許的。如果 http://example.com 嘗試載入 https://www.google-analytics.com/file.js,接著協議或埠允許被更改以便協議的提升。

style-src 'self' data: ;
     # Data-Uri 嵌入 CSS => 允許

在這個例子中,關鍵詞 data: 授權了在 CSS 檔案中 data 內嵌內容。

在 CSP 1 規範下,你也可以設定如下規則:

  • img-src 有效的圖片來源
  • connect-src 應用於 XMLHttpRequest(AJAX),WebSocket 或 EventSource
  • font-src 有效的字型來源
  • object-src 有效的外掛來源(例如,<object><embed><applet>
  • media-src 有效的 <audio><video> 來源

CSP 2 規範包含了如下規則:

  • child-src 有效的 web workers 和 元素來源,如 <frame><iframe> (這個指令用來替代 CSP 1 中廢棄了的 frame-src 指令)
  • form-action 可以作為 HTML <form> 的 action 的有效來源
  • frame-ancestors 使用 <frame><iframe><object><embed><applet> 內嵌資源的有效來源
  • upgrade-insecure-requests 命令使用者代理來重寫 URL 協議,將 HTTP 改到 HTTPS (為一些需要重寫大量陳舊 URL 的網站提供了方便)。

為了更好的向後相容一些廢棄的屬性,你可以簡單的複製當前指令的內容同時為那個廢棄的指令建立一個相同的副本。例如,你可以複製 child-src 的內容同時在 frame-src 中新增一份相同的副本。

CSP 2 允許你新增路徑到白名單中(CSP 1 只允許域名被新增到白名單中)。因此,相較於將整個 www.foo.com 域新增到白名單,你可以通過新增 www.foo.com/some/folder 這樣的路徑到白名單中來作更多的限制。這個需要瀏覽器中 CSP 2 的支援,但它很明顯更安全。

一個例子

我為 Web 2015 巴黎大會上我的演講 “CSP in Action”製作了一個簡單的例子。

在沒有 CSP 的情況下,頁面展示如下圖所示:

不是十分優美。要是我們啟用瞭如下的 CSP 指令又會怎樣呢?

<?php
    header("Content-Security-Policy: 
      default-src 'self' ;
      script-src 'self' www.google-analytics.com stats.g.doubleclick.net ; 
      style-src 'self' data: ;
      img-src 'self' www.google-analytics.com stats.g.doubleclick.net data: ;
      frame-src 'self' ;");
?>

瀏覽器將會作什麼呢?它會(非常嚴格的)在 CSP 基礎規則之下應用這些指令,這意味著任何沒有在 CSP 指令中被授權允許的都將會被禁止(“blocked” 指的是不被執行、不被顯示並且不被使用在網站中)。

在 CSP 的預設設定中,內聯指令碼和樣式是不被授權的,意味著每一個 <script>onclick 事件屬性或 style 屬性都將會被禁止。你可以使用 style-src 'unsafe-inline' ; 指令來授權使用內聯 CSS。

在一個支援 CSP 的現代瀏覽器中,上述示例看起來如下圖:

發生了什麼?瀏覽器應用了指令並且拒絕了所有沒有被授權的內容。它在瀏覽器除錯終端中傳送了這些通知:

如果你依然不確定 CSP 的價值,請看一下 Aaron Gustafson 文章 “More Proof We Don't Control Our Web Pages”。

當然,你可以使用比我們在示例中提供的更嚴格的指令:

  • 設定 default-src 為 'none'
  • 為每條規則指定你的設定
  • 為請求的檔案指定它的絕對路徑

更多關於 CSP 的資訊

支援

CSP 不是一個需要複雜的配置才能正常工作的每日構建特性。CSP 1 和 2 是候選推薦標準!瀏覽器可以非常完美的支援 CSP 1

CSP 2 是較新的規範,因此對它的支援會少那麼一點。

現在 CSP 3 還是一個早期草案,因此還沒有被支援,但是你依然可以使用 CSP 1 和 2 來做一些重大的事。

其他需要考慮的因素

CSP 被設計用來降低跨站指令碼攻擊(XSS)的風險,這就是不建議開啟內聯指令碼和 script-src 指令的原因。Firefox 對這個問題做了很好的說明:在瀏覽器中,敲擊 Shift + F2 並且鍵入 security csp,它就會向你展示指令和對應的建議。這裡有一個在 Twitter 網站中應用的例子:

如果你確實需要使用內聯指令碼和樣式的話,另一種可能就是生成一份雜湊值。例如,我們假定你需要使用如下的內聯指令碼:

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

你應該在 script-src 指令中新增 sha256-qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng= 作為有效來源。這個雜湊值用下面的 PHP 指令碼執行獲得的結果:

<?php
    echo base64_encode(hash('sha256', "alert('Hello, world.');", true));
?>

我在前文中說過 CSP 被設計用來降低 XSS 風險,我還得加上“……與降低未經請求內容的風險。”伴隨著 CSP 的使用,你必須知道你內容的來源是哪裡它們在你的前端都作了些什麼(內聯樣式,等)。CSP 同時可以幫助你讓貢獻者、開發人員和其他人員來遵循你內容來源的規則!

現在你的問題就只是,“不錯,這很好,但是我們如何在生產環境中使用它呢?”

如何在現實世界中使用它

想要在第一次使用 CSP 之後就失望透頂的方法就是在生產環境中測試。不要想當然的認為,“這會很簡單。我的程式碼是完美並且相當清晰的。”不要這樣作。我這樣幹過。相信我,這相當的蠢。

正如我之前說明的,CSP 指令由 CSP 頭部來啟用,這中間沒有過渡階段。你恰恰就是其中的薄弱環節。你可能會忘記授權某些東西或者遺忘了你網站中的一小段程式碼。CSP 不會饒恕你的疏忽。然而,CSP 的兩個特性將這個問題變得相當的簡單。

report-uri

還記得 CSP 傳送到終端中的那些通知麼?report-uri 指令可以被用來告訴瀏覽器傳送那些通知到指定的地址。報告以 JSON 格式送出。

report-uri /csp-parser.php ;

因此,我們可以在 csp-parser.php 檔案中處理有瀏覽器送出的資料。這裡有一個由 PHP 實現的最基礎的例子:

$data = file_get_contents('php://input');

    if ($data = json_decode($data, true)) {
     $data = json_encode(
      $data,
      JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
      );
     mail(EMAIL, SUBJECT, $data);
    }

這個通知將會被轉換成為一封郵件。在開發過程中,你可能不會需要比這更復雜的其它東西。

對於一個生產環境(或者是一個有較多訪問的開發環境),你應該使用一種比郵件更好的收集資訊的方式,因為這種方式在節點上沒有驗證和速率限制,並且 CSP 可能變得亂哄哄的。只需想像一個會產生 100 個 CSP 通知(例如,一個從未授權來源展示圖片的指令碼)並且每天會被瀏覽 100 次的頁面,那你就會每天收到 10000 個通知啊!

例如 report-uri.io 這樣的服務可以用來簡化你的通知管理。你也可以在 GitHub上看一些另外的使用 report-uri (與資料庫搭配,新增一些優化,等)的簡單例子。

report-only

正如我們所見的,最大的問題就是在使用和不使用 CSP 之間沒有中間地帶。然而,一個名為 report-only 的特性會傳送一個稍有不同的頭部:

<?php
    header("Content-Security-Policy-Report-Only: <your directives>");
?>

總的來說,這個頭部就是告訴瀏覽器,“表現得似乎所有的 CSP 指令都被應用了,但是不禁止任何東西。只是傳送通知給自己。”這是一種相當棒的測試指令的方式,避免了任何有價值的東西被禁止的風險。

report-onlyreport-uri 的幫助下你可以毫無風險的測試 CSP 指令,並且可以實時的監控網站上一切與 CSP 相關的內容。這兩個特性對部署和維護 CSP 來說真是相當的有用!

結論

為什麼 CSP 很酷

CSP 對你的使用者來說是尤其重要的:他們在你的網站上不再需要遭受任何的未經請求的指令碼,內容或 XSS 的威脅了。

對於網站維護者來說 CSP 最重要的優勢就是可感知。如果你對圖片來源設定了嚴格的規則,這時一個指令碼小子嘗試在你的網站上插入一張未授權來源的圖片,那麼這張圖片就會被禁止,並且你會在第一時間收到提醒。

開發者也需要確切的知道他們的前端程式碼都在做些什麼,CSP 可以幫助他們掌控一切。會促使他們去重構他們程式碼中的某些部分(避免行內函數和樣式,等)並且促使他們遵循最佳實踐。

如何讓 CSP 變得更酷

諷刺的是,CSP 在一些瀏覽器中過分的高效了,在和書籤欄小程式一起使用時會產生一些 bug。因此,不要更新你的 CSP 指令來允許書籤欄小程式。我們無法單獨的責備任何一個瀏覽器;它們都有些問題:

  • Firefox
  • Chrome (Blink)
  • WebKit

大多數情況下,這些 bug 都是禁止通知中的誤報。所有的瀏覽器提供者都在努力解決這些問題,因此我們可以期待很快就會被解決。無論怎樣,這都不會成為你使用 CSP 的絆腳石。


via: https://www.smashingmagazine.com/2016/09/content-security-policy-your-future-best-friend/

作者:Nicolas Hoffmann 譯者:wcnnbdk1 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

相關文章