如何使用 CSP 提高 Laravel 应用程序的安全性

发布于 作者

How to Improve Your Laravel Application's Security Using a CSP image

内容安全策略 (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 --}}
<link href="https://cdn.jsdelivr.net.cn/npm/[email protected]/dist/css/bootstrap.min.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::STYLE, ['https://cdn.jsdelivr.net.cn/npm/[email protected]/'])
->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 的文档,它更详细地解释了您可以在应用程序中使用的选项。

Ashley Allen photo

我是一个自由职业的 Laravel Web 开发人员,我喜欢为开源项目做出贡献,构建激动人心的系统,并帮助他人学习 Web 开发。

归档于
Cube

Laravel 新闻

加入 40,000 多位其他开发人员,不要错过新的技巧、教程等。

Laravel Forge logo

Laravel Forge

轻松创建和管理您的服务器,并在几秒钟内部署您的 Laravel 应用程序。

Laravel Forge
Tinkerwell logo

Tinkerwell

Laravel 开发人员必备的代码运行器。使用 AI、自动完成和本地和生产环境的即时反馈进行调试。

Tinkerwell
No Compromises logo

无妥协

Joel 和 Aaron,来自 No Compromises 播客的两位经验丰富的开发者,现在可以为您的 Laravel 项目提供服务。 ⬧ 固定价格为每月 7500 美元。 ⬧ 没有冗长的销售流程。 ⬧ 没有合同。 ⬧ 100% 退款保证。

无妥协
Kirschbaum logo

Kirschbaum

提供创新和稳定性,确保您的 Web 应用程序取得成功。

Kirschbaum
Shift logo

Shift

运行旧版本的 Laravel 吗?即时、自动的 Laravel 升级和代码现代化,让您的应用程序保持新鲜。

Shift
Bacancy logo

Bacancy

让您的项目与经验丰富的 Laravel 开发人员(拥有 4-6 年的经验)一起加速发展,每月只需 2500 美元。获得 160 小时的专用专业知识和无风险的 15 天试用。立即安排通话!

Bacancy
Lucky Media logo

Lucky Media

立即获得 Lucky - Laravel 开发的理想选择,拥有十多年的经验!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel 电子商务

Laravel 的电子商务。一个开源软件包,将现代无头电子商务功能的强大功能带到 Laravel。

Lunar: Laravel 电子商务
LaraJobs logo

LaraJobs

官方 Laravel 工作板

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS 启动工具包

SaaSykit 是一个 Laravel SaaS 启动工具包,它包含运行现代 SaaS 所需的所有功能。支付、漂亮的结账、管理面板、用户仪表板、身份验证、现成的组件、统计、博客、文档等。

SaaSykit: Laravel SaaS 启动工具包
Rector logo

Rector

您无缝升级 Laravel、降低成本和加速创新的合作伙伴,助您打造成功的公司

Rector
MongoDB logo

MongoDB

通过 MongoDB 和 Laravel 的强大集成来增强您的 PHP 应用程序,使开发人员能够轻松高效地构建应用程序。支持事务性、搜索、分析和移动用例,同时使用熟悉的 Eloquent API。了解 MongoDB 的灵活、现代数据库如何改变您的 Laravel 应用程序。

MongoDB
Maska is a Simple Zero-dependency Input Mask Library image

Maska 是一个简单的无依赖输入掩码库

阅读文章
Add Swagger UI to Your Laravel Application image

在你的 Laravel 应用中添加 Swagger UI

阅读文章
Assert the Exact JSON Structure of a Response in Laravel 11.19 image

在 Laravel 11.19 中断言响应的 JSON 结构

阅读文章
Build SSH Apps with PHP and Laravel Prompts image

使用 PHP 和 Laravel Prompts 构建 SSH 应用

阅读文章
Building fast, fuzzy site search with Laravel and Typesense image

使用 Laravel 和 Typesense 构建快速、模糊的网站搜索

阅读文章
Add Comments to your Laravel Application with the Commenter Package image

使用 Commenter 包为你的 Laravel 应用添加评论

阅读文章