spatie / laravel-csp
向 Laravel 应用的响应中添加 CSP 头
Requires
- php: ^8.1
- illuminate/http: ^9.0|^10.0|^11.0
- illuminate/support: ^9.0|^10.0|^11.0
- spatie/laravel-package-tools: ^1.11
Requires (Dev)
- mockery/mockery: ^1.3.3
- orchestra/testbench: ^7.0|^8.0|^9.0
- pestphp/pest: ^1.23.0|^2.34.0
- roave/security-advisories: dev-master
This package is auto-updated.
Last update: 2024-09-20 13:41:54 UTC
README
默认情况下,网页上的所有脚本都可以向任何网站发送和获取数据。这可能会引发安全问题。想象一下,如果你的一个 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.policy
和csp.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)。有关更多信息,请参阅 许可证文件。