# 内容安全策略

## 什么是内容安全策略？

防御 XSS 攻击的最佳手段之一是在网站中实施内容安全策略（CSP）。为此需要明确授权 HTML 中包含的各类资源，如图片、样式表和 JavaScript 文件等内容源。浏览器将自动拦截未经明确批准的来源内容。此类授权规则通过响应中的 `Content-Security-Policy` 标头进行定义，并提供多种配置选项。

这听起来很复杂，在某些网站上确实如此。但对于许多内容均由同一域名（如 http://example.com）提供的简单站点而言，集成 CSP 非常容易。

由于这是一个复杂的主题，本用户指南不会展开所有细节。如需深入了解，建议访问以下站点：

- 内容安全策略主站

- W3C 规范

- HTML5Rocks 的介绍

- SitePoint 的文章

## 启用 CSP

重要

调试工具栏 可能会使用 Kint，后者会输出内联脚本。因此，启用 CSP 后，系统会自动为调试工具栏输出 CSP nonce。但如果未使用 CSP nonce，CSP 标头会变为非预期内容，且行为与生产环境不同；如需验证 CSP 行为，请关闭调试工具栏。

默认情况下，CSP 支持处于关闭状态。要在应用中启用该支持，请编辑 app/Config/App.php 中的 `CSPEnabled` 值：

    <?php

    namespace Config;

    use CodeIgniter\Config\BaseConfig;

    class App extends BaseConfig
    {
        // ...

        public bool $CSPEnabled = true;
    }

启用后，响应对象将包含 `CodeIgniter\HTTP\ContentSecurityPolicy` 实例。系统会将 app/Config/ContentSecurityPolicy.php 中的配置值应用到该实例；若运行时无需修改，发送格式正确的响应头后即可完成操作。

启用 CSP 后，HTTP 响应中会添加两行标头：一行是 Content-Security-Policy 标头，包含明确允许的内容类型或来源策略；另一行是 Content-Security-Policy-Report-Only 标头，用于指定允许的内容类型或来源，并将触发规则的情况上报至指定的目的地。

本实现提供了一种默认处理方式，可通过 `reportOnly()` 方法进行更改。如下所示，向 CSP 指令添加新条目时，该条目会被添加到对应的拦截型或防御型 CSP 标头中。在调用添加方法时，通过提供可选的第二个参数，即可针对单次调用覆盖此默认行为。

## 运行时配置

如果应用需要在运行时进行更改，可在控制器中通过 `$this->response->getCSP()` 访问该实例。

该类包含多个方法，与需要设置的标头值一一对应。下面展示了不同的参数组合示例，所有方法均接受指令名或指令名数组作为参数：

    <?php

    // get the CSP instance
    $csp = $this->response->getCSP();

    // specify the default directive treatment
    $csp->reportOnly(false);

    // specify the origin to use if none provided for a directive
    $csp->setDefaultSrc('cdn.example.com');

    // specify the URL that "report-only" reports get sent to
    $csp->setReportURI('http://example.com/csp/reports');

    // specify that HTTP requests be upgraded to HTTPS
    $csp->upgradeInsecureRequests(true);

    // add types or origins to CSP directives
    // assuming that the default treatment is to block rather than just report
    $csp->addBaseURI('example.com', true); // report only
    $csp->addChildSrc('https://youtube.com'); // blocked
    $csp->addConnectSrc('https://*.facebook.com', false); // blocked
    $csp->addFontSrc('fonts.example.com');
    $csp->addFormAction('self');
    $csp->addFrameAncestor('none', true); // report this one
    $csp->addImageSrc('cdn.example.com');
    $csp->addMediaSrc('cdn.example.com');
    $csp->addManifestSrc('cdn.example.com');
    $csp->addObjectSrc('cdn.example.com', false); // reject from here
    $csp->addPluginType('application/pdf', false); // reject this media type
    $csp->addScriptSrc('scripts.example.com', true); // allow but report requests from here
    $csp->addStyleSrc('css.example.com');
    $csp->addSandbox(['allow-forms', 'allow-scripts']);

    // the following CSP3 directives are available in v4.7.0 and later
    $csp->addScriptSrcAttr('trusted.com');
    $csp->addScriptSrcElem('trusted.com');
    $csp->addStyleSrcAttr('trusted.com');
    $csp->addStyleSrcElem('trusted.com');
    $csp->addWorkerSrc('workers.example.com');

各个 "add" 方法的第一个参数为适当的字符串值，或其数组。

### 仅报告

`reportOnly()` 方法用于指定后续来源的默认报告处理方式，除非另行覆盖。

例如，可以指定允许 youtube.com，然后提供多个允许但会被报告的来源：

    <?php

    // get the CSP instance
    $csp = $this->response->getCSP();

    $csp->addChildSrc('https://youtube.com'); // allowed
    $csp->reportOnly(true);
    $csp->addChildSrc('https://metube.com'); // allowed but reported
    $csp->addChildSrc('https://ourtube.com', false); // allowed

### 上报指令

若要指定报告发送的 URL，可使用 `setReportURI()` 方法。

Added in version 4.7.0.

CSP Level 3 弃用了 `report-uri` 指令，转而推荐使用 `report-to`。因此，可使用 `setReportToEndpoint()` 方法为 CSP 报告设置上报地址。在添加此指令前，请确保已通过 `addReportingEndpoints()` 方法定义了上报地址。

    <?php

    // get the CSP instance
    $csp = $this->response->getCSP();

    $csp->setReportURI('https://example.com/csp-reports');

    // Starting in v4.7.0, you can use the setReportToEndpoint() method
    // to set the reporting endpoint for CSP reports
    $csp->addReportingEndpoints([
        'default' => 'https://example.com/csp-reports',
        'reports' => 'https://example.com/other-csp-reports',
    ]);
    $csp->setReportToEndpoint('default');

为向后兼容不支持 `report-to` 指令的浏览器，在使用 `setReportToEndpoint()` 方法时，CodeIgniter4 也会自动设置 `report-uri` 指令。

### 清除指令

如需清除现有 CSP 指令，可使用 `clearDirective()` 方法：

    <?php

    // get the CSP instance
    $csp = $this->response->getCSP();

    $csp->clearDirective('style-src');

## 内联内容

网站页面可能因包含用户生成的内容，导致内联脚本和样式未能获得有效保护。为此，CSP 允许在 `<style>` 和 `<script>` 标签中指定 nonce（随机数），并将这些值添加到响应头中。

### 使用占位符

在实际开发中，手动处理这些随机数非常繁琐，而采用动态生成的方式安全性最高。为简化操作，只需在标签中加入 `{csp-style-nonce}` 或 `{csp-script-nonce}` 占位符，系统便会自动完成转换:

    // 原始代码
    <script {csp-script-nonce}>
        console.log("由于不包含 nonce 属性，此脚本将无法运行");
    </script>

    // 转换后
    <script nonce="Eskdikejidojdk978Ad8jf">
        console.log("由于不包含 nonce 属性，此脚本将无法运行");
    </script>

    // 或者
    <style {csp-style-nonce}>
        . . .
    </style>

警告

若攻击者注入了类似 `<script {csp-script-nonce}>` 的字符串，此功能可能会将其转换为真实的 nonce 属性。为规避风险，可通过 app/Config/ContentSecurityPolicy.php 中的 `$scriptNonceTag` 和 `$styleNonceTag` 属性自定义占位符字符串。

### 使用函数

若不希望使用上述自动替换功能，可将 app/Config/ContentSecurityPolicy.php 中的 `$autoNonce` 设置为 `false`。

此时，可改用 `csp_script_nonce()` 和 `csp_style_nonce()` 函数:

    // 原始代码
    <script <?= csp_script_nonce() ?>>
        console.log("由于不包含 nonce 属性，此脚本将无法运行");
    </script>

    // 转换后
    <script nonce="Eskdikejidojdk978Ad8jf">
        console.log("由于不包含 nonce 属性，此脚本将无法运行");
    </script>

    // 或者
    <style <?= csp_style_nonce() ?>>
        . . .
    </style>