Настройка CSP в Laravel и Vite

Источник: «CSP in Laravel with Vite»
В этой статье я покажу как настроить заголовки Content Security Policy с помощью Laravel и Vite. Предполагается, что у вас есть некоторый опыт работы с Laravel и Vite.

Что такое CSP

CSP — это функция безопасности, используемая в веб-браузерах для защиты от XSS и других типов атак. Это политика, которую владелец веб-сайта может указать в HTTP-заголовке веб-страницы, чтобы указать браузеру пользователя, какие источники контента разрешены для загрузки страницы.

CSP работает, позволяя владельцам веб-сайтов указывать белый список надёжных источников контента, таких как сценарии, таблицы стилей, изображения и другие ресурсы, и предотвращать загрузку любого контента из источников, не входящих в белый список. Это помогает предотвратить внедрение злоумышленниками вредоносного кода на страницу или кражу конфиденциальных данных пользовательских данных, заставляя браузер загружать контент только из надёжных источников.

Где настраивается CSP

CSP-заголовки можно настроить в программе с помощью кода или на сервере. Настройка на сервере может привести к некоторым ограничениям, таким как невозможность динамического применения одноразовых номеров или хэшей, необходимость изменять конфигурацию сервера каждый раз при изменении источников контента и т.д. Поэтому рекомендуется делать это на уровне кода для большей гибкости.

Используя код, мы можем применять CSP двумя способами: один — с помощью мета-тегов внутри элемента <head> HTML-документа, а другой — путём добавления заголовков в ответы нашего сервера.

CSP в Laravel

В этой статье я покажу, как можно настроить CSP заголовки в автономной установке Laravel, а также в Laravel с некоторыми интерфейсными фреймворками, связанными с Vite. Поскольку Laravel предоставляет простой и гибкий объект ответа, мы будем использовать его для установки наших CSP заголовков с помощью пакета.

Мы будем использовать пакет Laravel spatie/laravel-csp для настройки CSP в приложении Laravel. Вы можете следовать инструкциям по установке на странице GitHub. Установите пакет и опубликуйте предоставленную конфигурацию CSP. Также, как показано на странице GitHub, зарегистрируйте Spatie\Csp\AddCspHeaders::class в своей web или api (если требуется) middleware группах.

Этот пакет настраивает CSP заголовки с помощью Policy файлов. Пакет предоставляет предварительно настроенную базовую политику (Basic policy) из коробки. Для многих этого может быть достаточно, но я буду создавать собственный Policy файл, чтобы добавить собственные источники.

Создайте каталог Support внутри каталога app/. В каталоге Support создайте файл с именем CSP.php. Мы добавим пользовательские политики в этот файл.

<?php

namespace App\Support;

use Spatie\Csp\Directive;
use Spatie\Csp\Keyword;
use Spatie\Csp\Policies\Basic;
use Spatie\Csp\Value;
use Spatie\Csp\Scheme;

class CSP extends Basic
{
public function configure()
{
parent::configure();

$this
->addDirective(Directive::IMG, [
'https://images.google.com',
]);
}
}

В моём пользовательском файле конфигурации выше я использовал Basic политику предоставленную пакетом, в качестве отправной точки и добавил новую директиву для img-src, позволяющую обслуживать изображения с https://images.google.com в моём приложении. После добавления этой конфигурации заголовки ответа CSP из моего приложения будут содержать заголовки, настроенные в Basic и моей пользовательской политике. Конфигурация в Basic политике, предоставляемой пакетов, выглядит следующим образом:

<?php

namespace Spatie\Csp\Policies;

use Spatie\Csp\Directive;
use Spatie\Csp\Keyword;

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);
}
}

Узнать больше о директивах, их значениях и целях, вы можете в MDN Web Docs

В последних двух строках Basic политики мы видим, как addNonceForDirective применяется к директивам сценария и стиля. Это позволяет нашему приложению генерировать одноразовое значение и применять его к нашим встроенным CSS и JS.

Nonce и хэши

Nonce — это случайно сгенерированная строка, которая добавляется в заголовок HTTP ответа. Одно и то же значение nonce добавляется к встроенным сценариям JavaScript и CSS, что бы разрешить их встроенное выполнение, поскольку по умолчанию CSP не разрешает встроенное выполнение сценариев. Значение nonce уникально для каждого запроса и ответа.

Хэши в CSP имеют такой же варианта использования, что и nonce, но хэши — это криптографические хэш-значения, вычисляемые с использованием встроенного содержимого CSS и JS в приложении. Хэш-значение для кодовой базы фиксируется до тех пор, пока не изменится базовый CSS или JS.

Мы создали файл пользовательской политики, но ещё не применили его. Чтобы сделать наш пользовательский файл политики активным, нужно изменить файл конфигурации csp.php в каталоге config.

<?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' => App\Support\CSP::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,
];

В этом файле конфигурации измените значение ключа policy на App\Support\CSP::class и сохраните его.

Если вы используете какие-либо встроенные стили или скрипты в своих blade-файлах Laravel, необходимо добавить значение nonce в эти теги, чтобы разрешить их встроенное выполнение. Например:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style nonce="{{csp_nonce()}}">
body {
background: #fff;
}
</style>
<script nonce="{{csp_nonce()}}">
console.log('Hello World');
</script>
</head>
<body>
<h1>This is simple Laravel Blade page.</h1>
</body>
</html>

Теперь, с этой настройкой, вы сможете увидеть настроенные заголовки CSP, добавленные к ответу для каждого запроса.

Интеграция фронтенда

До сих пор мы добавляли заголовки CSP для нашей автономной установки Laravel. Если у вас есть какие-либо интерфейсные фреймворки, такие, как React или Vue, установленные вместе с Laravel, нам нужно настроить несколько вещей, чтобы всё это заработало. Я предполагаю, что вы используете Vite в качестве инструмента сборки.

Как упоминалось выше, для встроенного выполнения стилей и скриптов нужно добавить значение nonce. При интеграции React или Vue в наше приложение Laravel они также внедряют сценарии JavaScript в наш blade-файл, где мы должны добавить nonce, чтобы они смогли выполниться.

Для этого нужно добавить meta свойство с нашим значением nonce в заголовок blade-файла, а также настроить генератор nonce с помощью Vite в конфигурационном фале config\csp.php.

Вы можете добавить meta свойство CSP, как показано ниже, в файле welcome.blade.php:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="csp-nonce" content="{{csp_nonce()}}">
<style nonce="{{csp_nonce()}}">
body {
background: #fff;
}
</style>
<script nonce="{{csp_nonce()}}">
console.log('Hello World');
</script>
{{-- React Frontend Scripts --}}
@viteReactRefresh
@vite('resources/js/app.jsx')
</head>
<body>
<h1>This is simple Laravel Blade page.</h1>
</body>
</html>

Теперь, когда мы добавили наше meta-свойство, давайте настроим генератор nonce. Для генерации nonce Vite предоставляет, из коробки, метод по умолчанию useCspNonce(). Создайте файл с именем ViteNonceGenerator.php внутри директории app\Support и добавьте следующий код:

<?php

namespace App\Support;

use Illuminate\Support\Facades\Vite;
use Spatie\Csp\Nonce\NonceGenerator;

class ViteNonceGenerator implements NonceGenerator
{
public function generate(): string
{
return Vite::useCspNonce();
}
}

Теперь добавьте этот файл в качестве нашего генератора nonce в файл config\csp.php.

...
/*
* The class responsible for generating the nonces used in inline tags and headers.
*/

'nonce_generator' => App\Support\ViteNonceGenerator::class,
...

После всего этого вы должны увидеть заголовки CSP в своём приложении без каких-либо проблем. Если вы посмотрите свой HTML DOM в браузере, вы сможете увидеть значение nonce, добавленное ко всем стилям и скриптам. Если вы не видите значение nonce, не беспокойтесь, большинство современных браузеров скрывает значение и просто показывает имя свойства, это означает, что вы успешно настроили правильное создание nonce в заголовке и сценариях.

Пример значения CSP заголовка сгенерированного с помощью вышеуказанных настроек:

base-uri 'self';connect-src 'self';default-src 'self';form-action 'self';img-src 'self' https://images.google.com;media-src 'self';object-src 'none';script-src 'self' 'nonce-EIY6JvzINHbS6VfUoJoET6L0Bldk2llgAUjQ9VdK';style-src 'self' 'nonce-EIY6JvzINHbS6VfUoJoET6L0Bldk2llgAUjQ9VdK'

Спасибо за чтение.

Дополнительные материалы к статье

Дополнительные материалы

Предыдущая Статья

Использование `declare(strict_types=1)` для повышения надежности кода

Следующая Статья

Новое в Symfony 6.3 — Улучшения DX (Часть 2)