如何使用 CSP 提高 Laravel 应用程序的安全性
发布于 作者 Ashley Allen
内容安全策略 (CSP) 是一种改善 Laravel 应用程序安全性的好方法。它们允许您将脚本、样式和其他资产的来源列入白名单,这些资产可以由您的网页加载。这可以防止攻击者将恶意代码注入您的视图(以及用户浏览器),并能使您更有把握地认为您使用的第三方资源确实是您想要使用的资源。
在本文中,我们将了解 CSP 是什么以及它们的用途。然后,我们将学习如何使用 spatie/laravel-csp 包将 CSP 添加到 Laravel 应用程序。我们还将简要介绍一些使将 CSP 添加到现有应用程序更轻松的技巧。
什么是内容安全策略?
简而言之,CSP 只是一组规则,通常从您的服务器返回到客户端的浏览器,通过响应中的 Content-Security-Policy
标头。它允许我们,作为开发人员,定义浏览器允许加载哪些资产。
因此,我们可以确信,用户只将我们认为对他们安全的图像、字体、样式和脚本加载到他们的浏览器中,并允许使用这些脚本。如果浏览器尝试加载未被允许的资产,它将被阻止。
使用配置良好的内容安全策略可以降低用户数据被盗和其他恶意行为的可能性,这些行为是通过诸如跨站点脚本 (XSS) 之类的攻击执行的。
CSP 可能变得非常复杂(尤其是在大型应用程序中),但它们是任何应用程序安全性的重要组成部分。
如何在 Laravel 中实现 CSP
正如我们已经提到的,CSP 只是从您的服务器返回到客户端浏览器的响应中的标头中的一组规则,有时也定义为 HTML 中的 <meta>
标签。这意味着您可以通过多种方式将 CSP 应用于您的应用程序。例如,您可以在服务器(例如 - Nginx)的配置中定义标头。但是,这可能很麻烦且难以管理,因此我发现,最好在应用程序级别管理策略。
通常,将策略添加到 Laravel 应用程序的最简单方法是使用 spatie/laravel-csp 包。让我们看看如何使用它以及它提供的不同选项。
安装
要开始使用 spatie/laravel-csp
包,我们首先需要使用以下命令通过 Composer 安装它
composer require spatie/laravel-csp
接下来,我们可以使用以下命令发布包的配置文件
php artisan vendor:publish --tag=csp-config
运行上面的命令应该会为您创建一个新的 config/csp.php
文件。
将策略应用于响应
现在包已安装,我们需要确保 Content-Security-Policy
标头已添加到您的 HTTP 响应中。根据您的应用程序,您可能希望通过几种不同的方式来执行此操作。
如果您希望将 CSP 应用于所有 Web 路由,可以将 Spatie\Csp\AddCspHeaders
中间件类添加到 app/Http/Kernel.php
文件中 $middlewareGroups
数组的 web
部分
// ... protected $middlewareGroups = [ 'web' => [ // ... \Spatie\Csp\AddCspHeaders::class, ], // ...
通过这样做,任何通过 web
中间件组运行的路由都将自动为您添加 CSP 标头。
如果您希望将 CSP 添加到单个路由或任何路由组,可以在 web.php
文件中使用中间件。例如,如果我们只想将中间件应用于特定路由,我们可以执行以下操作
use Spatie\Csp\AddCspHeaders; Route::get('example-route', 'ExampleController')->middleware(AddCspHeaders::class);
或者,如果我们希望将中间件应用于路由组,我们可以执行以下操作
use Spatie\Csp\AddCspHeaders; Route::middleware(AddCspHeaders::class)->group(function () { // Routes go here...});
默认情况下,如果您没有明确定义要与中间件一起使用的策略,则将使用发布的 config/csp.php
文件中 default
键中定义的策略。因此,如果您希望使用自己的默认策略,您可能需要更新该字段。
您可能有多个用于应用程序或网站的内容安全策略。例如,您可能有一个 CSP 用于网站的公共页面,另一个 CSP 用于网站的受限部分。这可能是由于您在这些地方使用了不同的资产集(例如脚本、样式和字体)。
因此,如果我们想明确定义应为特定路由使用的策略,我们可以执行以下操作
use App\Support\Csp\Policies\CustomPolicy;use Spatie\Csp\AddCspHeaders; Route::get('example-route', 'ExampleController')->middleware(AddCspHeaders::class.':'.CustomPolicy::class);
类似地,我们也可以在路由组中明确定义策略
use App\Support\Csp\Policies\CustomPolicy;use Spatie\Csp\AddCspHeaders; Route::middleware(AddCspHeaders::class.':'.CustomPolicy::class)->group(function () { // Routes go here...});
使用默认内容安全策略
该包附带一个默认的 Spatie\Csp\Policies\Basic
策略,该策略为我们定义了一些规则。该策略只允许我们从与应用程序相同的域加载图像、字体、样式和脚本。如果您只使用从自己的域加载的资产,此策略可能就足够了。
Basic
策略将创建一个 Content-Security-Policy
标头,它看起来像这样
base-uri 'self';connect-src 'self';default-src 'self';form-action 'self';img-src 'self';media-src 'self';object-src 'none';script-src 'self' 'nonce-YKXiTcrg6o4DuumXQDxYRv9gHPlZng6z';style-src 'self' 'nonce-YKXiTcrg6o4DuumXQDxYRv9gHPlZng6z'
创建您自己的内容安全策略
根据您的应用程序,您可能希望创建自己的策略,以允许加载 Basic
策略允许加载的其他资产。
正如我们已经提到的,CSP 中可以定义很多规则,并且它们很快就会变得相对复杂。因此,为了帮助您初步了解,我们将介绍一些您可能在自己的应用程序中使用的常见规则。
在本指南中,我们将假设我们有一个项目,该项目在页面上使用以下资产
- 一个在站点域上的
/js/app.js
位置可用的 JavaScript 文件。 - 一个在
https://unpkg.com/vue@3/dist/vue.global.js
位置可用的外部 JavaScript 文件。 - 内联 JavaScript - 但不是任何内联 JavaScript,我们只想允许我们明确允许运行的内联 JavaScript。
- 一个在站点域上的
/css/app.css
位置可用的 CSS 文件。 - 一个在
https://cdn.jsdelivr.net.cn/npm/[email protected]/dist/css/bootstrap.min.css
位置可用的外部 CSS 文件 - 一个在站点域上的
/img/hero.png
位置可用的图像。 - 一个在
https://laravel.net.cn/img/logotype.min.svg
位置可用的外部图像。
我们将创建一个内容安全策略,它只允许在我们的页面上加载上述项目。如果浏览器尝试加载任何其他资产,该请求将被阻止,并且不会加载。
页面的基本 Blade 视图可能看起来像这样
<html> <head> <title>CSP Test</title> {{-- Load Vue.js from the CDN --}} <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> {{-- Load some JS scripts from our domain --}} <script src="{{ asset('js/app.js') }}"></script> {{-- Load Bootstrap 5 CSS --}} rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous" > {{-- Load a CSS file from our own domain --}} <link rel="stylesheet" href="{{ asset('css/app.css') }}"> </head> <body> <h1>Csp Header</h1> <img src="{{ asset('img/hero.png') }}" alt="CSP hero image"> <img src="https://laravel.net.cn/img/logotype.min.svg" alt="Laravel logo"> {{-- Define some JS directly in our HTML. --}} <script> console.log('Loaded inline script!'); </script> {{-- Evil JS script which we didn't write ourselves and was injected by another script! --}} <script> console.log('Injected malicious script! ☠️'); </script> </body></html>
首先,我们需要创建一个扩展包中 `Spatie\Csp\Policies\Basic` 类的自定义策略类。没有特定的目录需要放置它,因此可以选择最适合应用程序的目录。我喜欢将其放置在 `app/Support/Csp/Policies` 目录中,但这只是我的偏好。所以我会创建一个新的 `app/Support/Csp/Policies/CustomPolicy.php` 文件。
namespace App\Support\Csp\Policies; use Spatie\Csp\Policies\Basic; class CustomPolicy extends Basic{ public function configure() { parent::configure(); // We can add our own policy directives here... }}
正如您从上面代码中的注释中看到的,我们可以在 `configure` 方法中放置我们自己的自定义指令。
因此,让我们添加一些指令并看看它们的作用。
namespace App\Support\Csp\Policies; use Spatie\Csp\Policies\Basic; class CustomPolicy extends Basic{ public function configure() { parent::configure(); $this->addDirective(Directive::SCRIPT, ['https://unpkg.com/vue@3/']) ->addDirective(Directive::IMG, 'https://laravel.net.cn'); }}
上面的策略将创建一个类似于下面的 `Content-Security-Policy` 标头。
base-uri 'self';connect-src 'self';default-src 'self';form-action 'self';img-src 'self';media-src 'self';object-src 'none';script-src 'self' 'nonce-3fvDDho6nNJ3xXPcK3VMsgBWjVTJzijk' https://unpkg.com/vue@3/;style-src 'self' 'nonce-3fvDDho6nNJ3xXPcK3VMsgBWjVTJzijk' https://cdn.jsdelivr.net.cn/npm/[email protected]/
在上面的示例中,我们定义了,任何从以 `https://unpkg.com/vue@3/` 开头的 URL 加载的 JS 文件都可以加载。这意味着我们的 Vue.js 脚本能够按预期加载。
我们还允许加载任何从以 `https://cdn.jsdelivr.net.cn/npm/[email protected]/` 开头的 URL 加载的 CSS 文件。
此外,我们还允许加载任何从以 `https://laravel.net.cn` 开头的 URL 获取的图像。
您可能还会想知道允许运行内联 JavaScript,以及从我们的域加载图像、CSS 和 JS 文件的指令在哪里。这些都包含在 `Basic` 策略中,因此我们不需要自己添加它们。所以我们可以保持我们的 `CustomPolicy` 紧凑,并且只添加我们需要的指令(通常用于外部资产)。
但是,目前,如果我们尝试运行我们的内联 JavaScript,它将无法正常工作。我们将在后面介绍如何解决这个问题。
虽然上面的规则有效,并且将允许我们的页面按预期加载,但您可能希望使规则更严格,以进一步提高页面的安全性。
让我们假设,由于某种未知的原因,一个恶意脚本设法进入了一个以 `https://unpkg.com/vue@3/` 开头的 URL,例如 `https://unpkg.com/vue@3/malicious-script.js`。由于我们规则的当前配置,此脚本将被允许在我们的页面上运行。因此,我们可能希望明确定义要允许加载的脚本的确切 URL。
我们将更新我们的策略,以包含我们想要加载的脚本、样式和图像的确切 URL。
namespace App\Support\Csp\Policies; use Spatie\Csp\Policies\Basic; class CustomPolicy extends Basic{ public function configure() { parent::configure(); $this->addDirective(Directive::SCRIPT, ['https://unpkg.com/vue@3/dist/vue.global.js']) ->addDirective(Directive::STYLE, ['https://cdn.jsdelivr.net.cn/npm/[email protected]/dist/css/bootstrap.min.css']) ->addDirective(Directive::IMG, 'https://laravel.net.cn/img/logotype.min.svg'); }}
上面的策略将创建一个类似于下面的 `Content-Security-Policy` 标头。
base-uri 'self';connect-src 'self';default-src 'self';form-action 'self';img-src 'self' https://laravel.net.cn/img/logotype.min.svg;media-src 'self';object-src 'none';script-src 'self' 'nonce-20gXfzoeWpjyg1ryUkWAma5gMWNN03xH' https://unpkg.com/vue@3/dist/vue.global.js;style-src 'self' 'nonce-20gXfzoeWpjyg1ryUkWAma5gMWNN03xH' https://cdn.jsdelivr.net.cn/npm/[email protected]/dist/css/bootstrap.min.css
通过使用上面的方法,我们可以大大提高页面的安全性,因为我们现在只允许加载我们想要加载的确切脚本、样式和图像。
但是,正如您可能想象的那样,对于较大的项目来说,这样做可能会变得很繁琐且耗时,因为您需要定义您从外部来源加载的每个资产。所以这是您需要根据项目情况考虑的事情。
在 CSP 中添加 Nonces
现在我们已经了解了如何允许加载外部资产,我们还需要了解如何允许运行内联脚本。
您可能还记得,我们在上面的 Blade 视图中拥有两个内联脚本块。
- 一个加载了我们想要运行的 JS。
- 一个由恶意脚本注入,并运行了一些恶意代码!
该脚本添加到 Blade 视图的底部,如下所示。
<html> <!-- ... --> {{-- Define some JS directly in our HTML. --}} <script> console.log('Loaded inline script!'); </script> {{-- Evil JS script which we didn't write ourselves and was injected by another script! --}} <script> console.log('Injected malicious script! ☠️'); </script> </body></html>
为了允许运行内联脚本,我们可以使用 "nonces"。Nonce 是为每个请求生成的随机字符串。该字符串随后被添加到 CSP 标头(通过我们正在扩展的 `Basic` 策略添加),并且加载的任何内联脚本都必须在其 `nonce` 属性中包含此 nonce。
让我们通过使用包提供的 `csp_nonce()` 助手来更新我们的 Blade 视图,以包含我们安全内联脚本的 nonce。
<html> <!-- ... --> {{-- Define some JS directly in our HTML. --}} <script nonce="{{ csp_nonce() }}"> console.log('Loaded inline script!'); </script> {{-- Evil JS script which we didn't write ourselves and was injected by another script! --}} <script> console.log('Injected malicious script! ☠️'); </script> </body></html>
这样做的结果是,我们的安全内联脚本现在将按预期运行。而没有 `nonce` 属性的注入脚本将被阻止运行。
使用 Meta 标签
不太可能,但您可能会发现您的 `Content-Security-Policy` 标头的内容超过了允许的最大长度。如果是这种情况,我们可以向我们的页面添加一个 `meta` 标签,以代替浏览器输出规则。
为此,您可以像这样向视图的 `<head>` 标签中添加包的 `@cspMetaTag` Blade 指令。
<html> <head> <!-- ... --> @cspMetaTag(App\Support\Csp\Policies\CustomPolicy::class) </head> <!-- ... --> </html>
使用上面 `CustomPolicy` 的示例,这将输出以下 `meta` 标签。
<meta http-equiv="Content-Security-Policy" content="base-uri 'self';connect-src 'self';default-src 'self';form-action 'self';img-src 'self' https://laravel.net.cn/img/logotype.min.svg;media-src 'self';object-src 'none';script-src 'self' 'nonce-oLbaz3rNhqvzKooMU8KpnqxgO9bFG1XQ' https://unpkg.com/vue@3/dist/vue.global.js;style-src 'self' 'nonce-oLbaz3rNhqvzKooMU8KpnqxgO9bFG1XQ' https://cdn.jsdelivr.net.cn/npm/[email protected]/dist/css/bootstrap.min.css">
在现有的 Laravel 应用程序中实施 CSP 的技巧
在现有的应用程序中添加 CSP 有时可能是一项非常困难的任务。通过实施过于严格的 CSP 或忘记添加仅在一个页面上使用的特定资产的规则,很容易破坏您的用户界面。我举手承认,我之前也做过这种事。
所以,如果你有机会在开始一个新应用程序时实施 CSP,我强烈建议你这样做。在构建应用程序的同时编写策略要容易得多。您忘记添加特定规则的可能性较小,您甚至可以在添加资产的同一个 git 提交中添加策略规则,以便您将来可以轻松地跟踪它。
但是,如果您将 CSP 添加到现有的应用程序中,您可以做一些事情来使自己和您的用户更容易完成此过程。
首先,您可以为您的策略启用 "仅报告" 模式。这允许您定义您的策略,但只要任何规则被违反(例如,加载未被允许加载的资产),就会将报告发送到给定的 URL,而不是阻止资产加载。通过这样做,它允许您创建您想要的 CSP,并在您的生产环境中对其进行测试,而不会破坏您的应用程序的用户。然后,您可以使用报告来识别您错过的任何资产,并将它们添加到您的策略中。
要为您的策略启用报告,您首先需要设置在检测到违规时应向其发出请求的 URL。您可以通过在您的 `.env` 文件中设置 `CSP_REPORT_URI` 字段来添加它,如下所示。
CSP_REPORT_URI=https://example.com/report-sent-here
然后,您可以在您的策略中使用 `reportOnly` 方法。如果我们要更新我们的策略以仅报告违规,它将看起来像这样。
namespace App\Support\Csp\Policies; use Spatie\Csp\Policies\Basic; class CustomPolicy extends Basic{ public function configure() { parent::configure(); $this->addDirective(Directive::SCRIPT, ['https://unpkg.com/vue@3/dist/vue.global.js']) ->addDirective(Directive::STYLE, ['https://cdn.jsdelivr.net.cn/npm/[email protected]/dist/css/bootstrap.min.css']) ->addDirective(Directive::IMG, 'https://laravel.net.cn/img/logotype.min.svg') ->reportOnly(); }}
使用 `reportOnly` 方法的结果是,将向响应中添加一个 `Content-Security-Policy-Report-Only` 标头,而不是 `Content-Security-Policy` 标头。上面的策略将生成一个类似于下面的标头。
report-uri https://example.com/report-sent-here;base-uri 'self';connect-src 'self';default-src 'self';form-action 'self';img-src 'self' https://laravel.net.cn/img/logotype.min.svg;media-src 'self';object-src 'none';script-src 'self' 'nonce-hI66wwieLS9inQh9GO4iaItVTFoPcNnj' https://unpkg.com/vue@3/dist/vue.global.js;style-src 'self' 'nonce-hI66wwieLS9inQh9GO4iaItVTFoPcNnj' https://cdn.jsdelivr.net.cn/npm/[email protected]/dist/css/bootstrap.min.css
经过一段时间(也许几天、几周或几个月),如果您没有收到任何报告,并且您确信该策略适合,您可以启用它。这意味着您仍然能够在发生违规时获取报告,但您也将能够获得实施策略的全部安全优势,因为任何违规都将被阻止。为此,您可以从您的策略类中删除 `reportOnly` 方法调用。
此外,您可能会发现按我们之前在本文中介绍的方式逐步提高规则的严格程度也很有用。因此,您可能只想在初始 CSP 中使用域或通配符,然后逐渐更改规则以使用更具体的 URL。
总而言之,我认为在现有应用程序中采用 CSP 的关键是逐步进行。当然可以一次性添加所有内容,但您可以通过采用更渐进的方式来减少错误和 bug 的可能性。
结论
希望这篇文章能让你对 CSP、它们解决的问题以及它们的工作原理有一个概述。您现在还应该知道如何在自己的 Laravel 应用程序中使用 spatie/laravel-csp 包来实施 CSP。
您可能还想查看 MDN 关于 CSP 的文档,它更详细地解释了您可以在应用程序中使用的选项。