spatie/laravel-csp

向 Laravel 应用的响应中添加 CSP 头

2.10.1 2024-09-20 13:39 UTC

README

Latest Version on Packagist GitHub Workflow Status Check & fix styling Total Downloads

默认情况下,网页上的所有脚本都可以向任何网站发送和获取数据。这可能会引发安全问题。想象一下,如果你的一个 JavaScript 依赖项将所有按键(包括密码)发送到第三方网站。

有人很容易隐藏这种行为,这几乎让你无法检测到(除非你手动阅读网站上所有的 JavaScript 代码)。为了更好地了解为什么你需要设置内容安全策略头部,请阅读 这篇优秀的博客文章,作者是 David Gilbertson

设置内容安全策略头部有助于解决这个问题。这些头部规定了你的网站允许联系哪些网站。这个包使你能够轻松设置正确的头部。

本说明文件并不旨在全面解释 CSP 及其指令的所有可能用法。我们强烈建议你在使用此包之前阅读 Mozilla 关于内容安全策略的文档。关于 CSP 的另一个很好的学习资源是 Stephen Rees-Carter 编写的 Larasec 通讯录版

支持我们

我们投入了大量资源来创建 最佳开源包。你可以通过 购买我们的付费产品之一 来支持我们。

我们非常感谢你从家乡寄来明信片,提到你正在使用我们哪个包。你可以在 我们的联系页面 上找到我们的地址。我们将所有收到的明信片发布在 我们的虚拟明信片墙上

安装

您可以通过 composer 安装此包

composer require spatie/laravel-csp

您可以使用以下命令发布配置文件:

php artisan vendor:publish --tag=csp-config

此文件的内容将被发布在 config/csp.php

return [

    /*
     * A policy will determine which CSP headers will be set. A valid CSP policy is
     * any class that extends `Spatie\Csp\Policies\Policy`
     */
    'policy' => Spatie\Csp\Policies\Basic::class,

    /*
     * This policy which will be put in report only mode. This is great for testing out
     * a new policy or changes to existing csp policy without breaking anything.
     */
    'report_only_policy' => '',

    /*
     * All violations against the policy will be reported to this url.
     * A great service you could use for this is https://report-uri.com/
     *
     * You can override this setting by calling `reportTo` on your policy.
     */
    'report_uri' => env('CSP_REPORT_URI', ''),

    /*
     * Headers will only be added if this setting is set to true.
     */
    'enabled' => env('CSP_ENABLED', true),

    /*
     * The class responsible for generating the nonces used in inline tags and headers.
     */
    'nonce_generator' => Spatie\Csp\Nonce\RandomString::class,
];

您可以通过在 http kernel 中注册 Spatie\Csp\AddCspHeaders::class 来将 CSP 头部添加到您应用的所有响应中。

// app/Http/Kernel.php

...

protected $middlewareGroups = [
   'web' => [
       ...
       \Spatie\Csp\AddCspHeaders::class,
   ],

或者,您也可以在路由或路由组级别上应用中间件。

// in a routes file
Route::get('my-page', 'MyController')->middleware(Spatie\Csp\AddCspHeaders::class);

您还可以将策略类作为参数传递给中间件

// in a routes file
Route::get('my-page', 'MyController')->middleware(Spatie\Csp\AddCspHeaders::class . ':' . MyPolicy::class);

给定的策略将覆盖配置文件中为该特定路由或路由组配置的策略。

用法

此包允许您定义 CSP 策略。CSP 策略决定了将设置在响应头部中的 CSP 指令。

一个 CSP 指令的示例是 script-src。如果此值为 'self' www.google.com,则您的网站只能从其自己的域名或 www.google.com 加载脚本。您可以在 Mozilla 优秀的开发者网站上找到 所有 CSP 指令的列表

根据规范,某些指令值需要用引号包围。例如,'self''none''unsafe-inline'。当使用 addDirective 函数时,您不需要手动用引号包围指令值。我们将自动添加引号。脚本/样式的散列值也将自动检测并包围引号。

// in a policy
...
   ->addDirective(Directive::SCRIPT, Keyword::SELF) // will output `'self'` when outputting headers
   ->addDirective(Directive::STYLE, 'sha256-hash') // will output `'sha256-hash'` when outputting headers
...

您可以在同一个指令中添加多个策略选项,将 addDirective 的第二个参数作为数组,或者将每个选项用一个或多个空格分隔的单个字符串。

// in a policy
...
   ->addDirective(Directive::SCRIPT, [
       Keyword::STRICT_DYNAMIC,
       Keyword::SELF,
       'www.google.com',
   ])
   ->addDirective(Directive::SCRIPT, 'strict-dynamic self  www.google.com')
   // will both output `'strict_dynamic' 'self' www.google.com` when outputting headers
...

也有一些情况,您不需要或不需要指定值,例如 upgrade-insecure-requests、block-all-mixed-content 等。在这种情况下,您可以使用以下值

// in a policy
...
    ->addDirective(Directive::UPGRADE_INSECURE_REQUESTS, Value::NO_VALUE)
    ->addDirective(Directive::BLOCK_ALL_MIXED_CONTENT, Value::NO_VALUE);
...

这将输出一个类似以下内容的 CSP

Content-Security-Policy: upgrade-insecure-requests;block-all-mixed-content

创建策略

csp 配置文件的 policy 键默认设置为 \Spatie\Csp\Policies\Basic::class。该类允许您的网站仅使用自己的图像、脚本和表单操作。这个类看起来是这样的

namespace App\Support;

use Spatie\Csp\Directive;
use Spatie\Csp\Value;

class Basic extends Policy
{
    public function configure()
    {
        $this
            ->addDirective(Directive::BASE, Keyword::SELF)
            ->addDirective(Directive::CONNECT, Keyword::SELF)
            ->addDirective(Directive::DEFAULT, Keyword::SELF)
            ->addDirective(Directive::FORM_ACTION, Keyword::SELF)
            ->addDirective(Directive::IMG, Keyword::SELF)
            ->addDirective(Directive::MEDIA, Keyword::SELF)
            ->addDirective(Directive::OBJECT, Keyword::NONE)
            ->addDirective(Directive::SCRIPT, Keyword::SELF)
            ->addDirective(Directive::STYLE, Keyword::SELF)
            ->addNonceForDirective(Directive::SCRIPT)
            ->addNonceForDirective(Directive::STYLE);
    }
}

您可以通过扩展这个类来允许从 www.google.com 获取脚本

namespace App\Support;

use Spatie\Csp\Directive;
use Spatie\Csp\Policies\Basic;

class MyCustomPolicy extends Basic
{
    public function configure()
    {
        parent::configure();
        
        $this->addDirective(Directive::SCRIPT, 'www.google.com');
    }
}

别忘了将 csp 配置文件中的 policy 键设置为策略的类名(在这种情况下,应该是 App\Support\MyCustomPolicy)。

使用内联脚本和样式

当使用 CSP 时,您必须明确允许使用内联脚本或样式。使用此包推荐的方法是使用一个 nonce。nonce 是一个针对每个请求唯一的数字。nonce 必须在 CSP 标题和 HTML 标签上的一个属性中指定。这样,攻击者就无法注入恶意脚本或样式。

首先,您必须将 nonce 添加到策略中正确的指令

// in a policy

public function configure()
  {
      $this
        ->addDirective(Directive::SCRIPT, 'self')
        ->addDirective(Directive::STYLE, 'self')
        ->addNonceForDirective(Directive::SCRIPT)
        ->addNonceForDirective(Directive::STYLE)
        ...
}

接下来,您必须将 nonce 添加到 HTML 中

{{-- in a view --}}
<style nonce="{{ csp_nonce() }}">
   ...
</style>

<script nonce="{{ csp_nonce() }}">
   ...
</script>

还有其他一些选项可以用于内联样式和脚本。请查看 Mozilla 开发者网站上的 CSP 文档 以获取更多信息。

与 Vite 的集成

在构建资产时,Laravel 的 Vite 插件可以 生成一个 nonce,您可以使用 Vite::cspNonce 来检索。您可以在自己的 NonceGenerator 中使用它。

namespace App\Support;

use Illuminate\Support\Str;
use Illuminate\Support\Facades\Vite;

class LaravelViteNonceGenerator implements NonceGenerator
{
    public function generate(): string
    {
        return Vite::cspNonce();
    }
}

别忘了在 csp 配置文件的 nonce_generator 键中指定您的 NonceGenerator 的完全限定类名。

或者,您可以指示 Vite 使用一个特定值作为 nonce。

namespace App\Support;

use Illuminate\Support\Str;
use Illuminate\Support\Facades\Vite;

class RandomString implements NonceGenerator
{
    public function generate(): string
    {
        $myNonce = ''; // determine the value for `$myNonce` however you want
    
        Vite::useCspNonce($myNonce);
        
        return $myNonce;
    }
}

将 CSP 策略作为元标签输出

在罕见的情况下,大型网站可能有许多外部连接,实际上导致 CSP 标题超过了最大标题大小。幸运的是,CSP 规范 允许将信息作为元标签输出到网页的 head 中。

为了支持这个用例,这个包提供了一个 @cspMetaTag blade 指令,您可以将它放在网站的 <head> 中。

<head>
    @cspMetaTag(App\Support\MyCustomPolicy::class)
</head>

当使用元标签 blade 指令时,您应该注意以下实现细节

  • 请注意,您应该手动传递我们想要输出元标签的策略的完全限定类名。这里的 csp.policycsp.report_only_policy 配置选项没有效果。
  • 因为 blade 文件没有访问 Response 对象,所以 shouldBeApplied 方法将不起作用。如果您已声明了 @cspMetaTag 指令,并且将 csp.enabled 配置选项设置为 true,则无论怎样,都将输出元标签。
  • 任何配置(如将策略设置为仅报告)都应该在策略的 configure 方法中完成,而不是依赖于 csp 配置文件中的设置。将会尊重 csp.report_uri 选项,因此无需手动配置。

报告 CSP 错误

在浏览器中

您可以将策略设置为仅报告模式,而不是直接阻止所有违规行为。在这种情况下,所有请求都将被发送,但所有违规行为都会显示在您喜欢的浏览器的控制台中。

要将策略设置为仅报告模式,只需在报告的 configure() 函数中调用 reportOnly()

public function configure()
{
    parent::configure();
    
    $this->reportOnly();
}

到外部 URL

任何违反策略的行为都可以报告给指定的 URL。您可以在 csp 配置文件的 report_uri 键中设置该 URL。一个专门为处理这些违规报告而构建的出色服务是 http://report-uri.io/

使用多个策略

为了测试对您的 CSP 策略的更改,您可以在 csp 配置键的 report_only_policy 中指定第二个策略。在 policy 中指定的策略将被强制执行,而在 report_only_policy 中指定的策略则不会。这对于测试新策略或现有 CSP 策略的更改而不会破坏任何内容非常有用。

使用 whoops

Laravel 配备了 whoops,这是一个错误处理框架,可以帮助您通过异常的漂亮可视化来调试您的应用程序。由于 whoops 无法对其使用的环境做出任何假设,因此它使用内联脚本和样式,除非您允许脚本和样式的 unsafe-inline,否则它将不起作用。

解决此问题的方法之一是在设置您的策略时检查 config('app.debug')。不幸的是,这存在忘记在所有 CSP 规则启用的情况下测试您的代码,并且在部署时应用程序崩溃的风险。或者,您可以通过向异常处理程序的 render 方法(通常位于 app/Exceptions/Handler.php)中添加以下内容,仅允许错误页面上的 unsafe-inline

$this->container->singleton(AppPolicy::class, function ($app) {
    return new AppPolicy();
});
app(AppPolicy::class)->addDirective(Directive::SCRIPT, Keyword::UNSAFE_INLINE);
app(AppPolicy::class)->addDirective(Directive::STYLE, Keyword::UNSAFE_INLINE);

其中 AppPolicy 是您的 CSP 策略名称。这也适用于其他所有情况,以在运行时更改策略,在这种情况下,应在服务提供程序而不是异常处理程序中进行单例注册。

请注意,unsafe-inline 仅在您没有同时发送 nonce 或 strict-dynamic 指令时才有效,因此要使用此解决方案,您必须指定所有内联脚本和样式的哈希值在 CSP 标头中。

另一种方法是,如果 Laravel 响应错误,则覆盖 Spatie\Csp\Policies\Policy::shouldBeApplied() 函数

namespace App\Services\Csp\Policies;

use Illuminate\Http\Request;
use Spatie\Csp;
use Symfony\Component\HttpFoundation\Response;

class MyCustomPolicy extends Csp\Policies\Policy
{
    public function configure()
    {
        // Add directives
    }
    
    public function shouldBeApplied(Request $request, Response $response): bool
    {
        if (config('app.debug') && ($response->isClientError() || $response->isServerError())) {
            return false;
        }

        return parent::shouldBeApplied($request, $response);
    }
}

这种方法完全禁用了 CSP,因此如果使用严格 CSP,则该方法也有效。

测试

您可以使用以下命令运行所有测试:

composer test

变更日志

有关最近更改的更多信息,请参阅 变更日志

贡献

有关详细信息,请参阅 贡献

安全性

如果您发现了关于安全的错误,请通过电子邮件发送到 security@spatie.be 而不是使用问题跟踪器。

鸣谢

许可证

MIT 许可证(MIT)。有关更多信息,请参阅 许可证文件