在laravel项目中提高安全性方式推荐:CSP内容安全策略

2023-06-01 00:00:00 安全性 安全策略 提高

内容安全策略(CSP)是提高你的Laravel应用程序安全性的一个好方法. 

他们允许你将脚本, 样式, 和其他资产的来源列入白名单,让你的网页可以加载。

这可以防止攻击者将恶意代码注入你的视图(以及你的用户的浏览器),并可以给你增加信心,你所使用的第三方资源是你想要使用的。

在这篇文章中,我们将看一下什么是CSP,以及它们能实现什么。

然后我们将看看如何使用spatie/laravel-csp包来为你的Laravel应用程序添加一个CSP。

我们还将简要介绍一些技巧,使添加CSP到一个现有的应用程序更容易。


什么是内容安全策略?

在最简单的术语中, CSP只是一组规则, 通常是通过响应中的Content-Security-Policy标头从服务器返回到客户端的浏览器. 它允许我们,作为开发者,定义浏览器允许加载哪些资产。

这样做的结果是,它可以让我们相信,我们的用户只向他们的浏览器加载我们认为可以安全加载并允许使用的图像、字体、样式和脚本。如果浏览器试图加载一个不被允许的资产,它将被阻止。

使用一个配置良好的内容安全策略可以减少用户数据被盗和其他使用跨站脚本(XSS)等攻击的恶意行为的可能性。

CSP可以变得非常复杂(特别是在较大的应用程序中),但它们是任何应用程序安全的一个重要组成部分。


如何在Laravel中实现一个CSP

正如我们已经提到的, CSP只是一组规则, 从你的服务器通过响应中的头返回到客户端的浏览器, 或者有时定义为HTML中的<meta>标签. 这意味着你有几种方法可以将CSP应用到你的应用程序中。例如,你可以在服务器(如Nginx)的配置中定义头信息。然而,这可能很麻烦,也很难管理,所以我发现在应用层面上管理策略会更容易。

通常情况下, 最简单的方法是使用spatie/laravel-csp包来添加策略到你的Laravel应用程序. 

所以让我们来看看我们如何使用它, 以及它为我们提供的不同选项.


安装csp

要开始使用spatie/laravel-csp包,我们首先需要通过Composer使用以下命令来安装它:

composer require spatie/laravel-csp

生成配置文件:config/csp.php,下面发布软件包的配置文件命令:

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

将策略应用到响应中

现在软件包已经安装完毕,我们需要确保在你的HTTP响应中加入Content-Security-Policy头。

根据你的应用,你可能想用几种不同的方式来做这件事。

如果你想把CSP应用到你所有的Web路由,

你可以把Spatie\Csp\AddCspHeaders中间件类,添加到你的app/Http/Kernel.php文件中的$middlewareGroups数组的Web部分:

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

这样做的结果是,任何通过你的网络中间件组运行的路由,将自动为你添加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文件的默认键中定义的策略。

因此,如果你想使用自己的默认策略,你可能想更新该字段。

你有可能为你的应用程序或网站有几个内容安全策略。

例如,你可能有一个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策略,已经为我们定义了一些规则。

该策略只允许我们从与我们的应用程序相同的域加载图像、字体、样式和脚本。

如果你只使用从你自己的领域加载的资产,这个策略可能对你来说已经足够了。

基本策略将创建一个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'

创建你自己的内容安全策略

根据你的应用,你可能想创建自己的策略,允许加载基本策略所允许的其他资产。

正如我们已经提到的,有很多规则可以在CSP中定义,而且它们可以很快变得相对复杂。

因此,为了帮助你简单了解,我们将看一下你可能会在自己的应用程序中使用的一些常见规则。


假设我们有一个项目,在页面上使用以下资产。

一个在网站域名上可用的JavaScript文件,地址是

/js/app.js

一个在外部可用的JavaScript文件:

https://unpkg.com/[email protected]/dist/vue.global.js

内联JavaScript - 但不是任何内联JavaScript,我们只想允许我们明确允许运行的内联JavaScript。

一个网站域名上的CSS文件:

/css/app.css

一个可在外部获得的CSS文件:

https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css

一张网站域名上的图片:

/img/hero.png

一张可从外部获得的图片:

https://laravel.com/img/logotype.min.svg

我们将创建一个内容安全策略,只允许在我们的页面上加载上述项目。

如果浏览器试图加载任何其他资产,该请求将被阻止,不会被加载。

该页面的基本Blade视图代码:

<html>
    <head>
        <title>CSP Test</title>
 
        {{-- Load Vue.js from the CDN --}}
        <script src="https://unpkg.com/[email protected]/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/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.com/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/[email protected]/'])
            ->addDirective(Directive::STYLE, ['https://cdn.jsdelivr.net/npm/[email protected]/'])
            ->addDirective(Directive::IMG, 'https://laravel.com');
    }
}

上述策略将创建一个看起来像这样的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/[email protected]/;style-src 'self' 'nonce-3fvDDho6nNJ3xXPcK3VMsgBWjVTJzijk' https://cdn.jsdelivr.net/npm/[email protected]/

在我们上面的例子中,我们已经定义了任何从以

https://unpkg.com/[email protected]/

开头的URL加载的JS文件都可以被加载。这意味着我们的Vue.js脚本将能够如期加载。

我们还允许从以

https://cdn.jsdelivr.net/npm/[email protected]/ 

开头的URL中加载任何CSS文件。

此外,我们还允许从以

https://laravel.com

开头的URL中获取的任何图片被加载。

你可能还想知道,允许运行内联JavaScript,以及从我们的域名加载图像、CSS和JS文件的指令在哪里。

这些都包含在基本策略中,所以我们不需要自己添加它们。

因此,我们可以保持我们的CustomPolicy良好和精简,只添加我们需要的指令(通常用于外部资产)。

然而,目前,如果我们试图运行我们的内联JavaScript,它将无法工作。我们将进一步介绍如何解决这个问题。尽管上面的规则是有效的,并允许我们的页面如期加载,但你可能想使规则更严格,以进一步提高页面的安全性。

让我们设想一下,由于任何未知的原因,一个恶意脚本设法进入一个以

https://unpkg.com/[email protected]/

开头的URL,例如

https://unpkg.com/[email protected]/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/[email protected]/dist/vue.global.js'])
            ->addDirective(Directive::STYLE, ['https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css'])
            ->addDirective(Directive::IMG, 'https://laravel.com/img/logotype.min.svg');
    }
}

上述策略将创建一个看起来像这样的Content-Security-Policy标头:

base-uri 'self';connect-src 'self';default-src 'self';form-action 'self';img-src 'self' https://laravel.com/img/logotype.min.svg;media-src 'self';object-src 'none';script-src 'self' 'nonce-20gXfzoeWpjyg1ryUkWAma5gMWNN03xH' https://unpkg.com/[email protected]/dist/vue.global.js;style-src 'self' 'nonce-20gXfzoeWpjyg1ryUkWAma5gMWNN03xH' https://cdn.jsdelivr.net/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>

为了允许内联脚本的运行,我们可以使用 "nonce"。nonce是一个随机字符串,为每个请求生成。

然后这个字符串被添加到CSP头(通过我们正在扩展的基本策略添加),任何被加载的内联脚本必须在其nonce属性中包括这个nonce。

让我们更新我们的Blade视图,通过使用软件包提供的csp_nonce()帮助器,为我们的安全内联脚本包括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属性的注入脚本将被阻止运行。


使用元标签

这不太可能,但你有可能发现你的内容安全政策头的内容超过了最大允许长度。

如果是这种情况,我们可以在我们的页面上添加一个元标签,为我们的浏览器输出规则。

要做到这一点,你可以将软件包的@cspMetaTag Blade指令添加到你的视图的<head>标签中,像这样:

<html>
    <head>
        <!-- ... -->
 
        @cspMetaTag(App\Support\Csp\Policies\CustomPolicy::class)
    </head>
 
    <!-- ... -->
 
</html>

以我们上面的CustomPolicy为例,这将输出以下元标签:

<meta http-equiv="Content-Security-Policy" content="base-uri 'self';connect-src 'self';default-src 'self';form-action 'self';img-src 'self' https://laravel.com/img/logotype.min.svg;media-src 'self';object-src 'none';script-src 'self' 'nonce-oLbaz3rNhqvzKooMU8KpnqxgO9bFG1XQ' https://unpkg.com/[email protected]/dist/vue.global.js;style-src 'self' 'nonce-oLbaz3rNhqvzKooMU8KpnqxgO9bFG1XQ' https://cdn.jsdelivr.net/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/[email protected]/dist/vue.global.js'])
            ->addDirective(Directive::STYLE, ['https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css'])
            ->addDirective(Directive::IMG, 'https://laravel.com/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.com/img/logotype.min.svg;media-src 'self';object-src 'none';script-src 'self' 'nonce-hI66wwieLS9inQh9GO4iaItVTFoPcNnj' https://unpkg.com/[email protected]/dist/vue.global.js;style-src 'self' 'nonce-hI66wwieLS9inQh9GO4iaItVTFoPcNnj' https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css

一段时间后(可能是几天、一周或几个月),如果你没有收到任何报告,而且你有信心该政策是合适的,你可以启用它。这意味着,当有违规行为时,你仍然能够得到报告,但你也能够得到实施该政策的全部安全好处,因为任何违规行为都会被阻止。要做到这一点,你可以从你的策略类中删除reportOnly方法调用。

此外,你可能也会发现逐步提高规则的严格程度是很有用的,就像我们在本文前面所讲的那样。

因此,你可能想在最初的CSP中只使用域名或通配符,然后逐渐改变规则,使用更具体的URL。

总而言之,我认为在你现有的应用程序中采用CSP的关键是要逐渐接近它。

一下子把它全部加进去是绝对可能的,但你可以通过采取更多的渐进方式来减少错误和漏洞的机会。


总结

希望这篇文章能给你一个关于CSP的概述, 他们解决的问题, 以及他们如何工作. 

你现在也应该知道如何使用spatie/laravel-csp包在你自己的Laravel应用程序中实现CSP.

https://github.com/spatie/laravel-csp

你可能还想看看MDN关于CSP的文档, 它解释了更多的选项供你在你的应用程序中使用.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy

转:

https://laravel-news.com/laravel-content-security-policies

相关文章