Laravel: Как улучшить безопасность приложения с CSP

Источник: «How to Improve Your Laravel Application`s Security Using a CSP»
В этой статье мы рассмотрим, что такое CSP и что он даёт. Затем рассмотрим, как использовать пакет для добавления CSP в Laravel приложении. Также кратко рассмотрим несколько советов по упрощению добавления CSP в существующее приложение.

Политики безопасности содержимого (CSP) — отличный способ повысить безопасность Laravel приложения. Они позволяют внести в белый список источники сценариев, стилей и других ресурсов, которые могут загружать веб-страницы. Это предотвращает внедрение атакующим вредоносного кода в представление (и, следовательно, в браузеры пользователей). И может дать дополнительную уверенность в том, что сторонние ресурсы, которые вы используете, — это то, что вы намеревались использовать.

В этой статье мы рассмотрим, что такое CSP: Политика безопасности контента и что она даёт. Затем мы рассмотрим, как использовать пакет spatie/laravel-csp для добавления CSP в Laravel приложение. Мы также кратко рассмотрим несколько советов по упрощению добавления CSP в существующее приложение.

Что такое Политика безопасности контента

Проще говоря, CSP — это просто набор правил, которые обычно возвращаются в ответе с вашего сервера в браузер клиента через заголовок Content-Security-Policy. Это позволяет нам, как разработчикам, определять, какие ресурсы разрешено загружать браузеру.

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

Использование хорошо настроенной политики безопасности контента может снизить вероятность кражи пользовательских данных и других злонамеренных действий, осуществляемых с использованием таки атак, как межсайтовый скриптинг/сценарии (XSS).

CSP могут стать очень сложными (особенно в больших приложениях), но они являются важной частью безопасности любого приложения.

Как реализовать CSP в Laravel

Как мы уже упоминали, CSP — просто набор правили, которые возвращаются с вашего сервера в браузер клиента через заголовок в ответе или иногда определяются как тег <meta> в HTML. Это означает, что существует несколько способов применения CSG в вашем приложении. Например, вы можете определить заголовки в конфигурации сервера (например, Nginx). Однако это может быть громоздким и сложным в управлении, поэтому я считаю, что проще управлять политикой на уровне приложения.

Как правило, самый простой способ добавить CSP в 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 ко всем вашим веб-маршрутам, вы можете добавить middleware класс Spatie\Csp\AddCspHeaders в веб-часть массива $middlewareGroups в файле app/Http/Kernel.php:

// ...

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

// ...

В результате, к любому маршруту, проходящему через вашу группу web middleware, автоматически добавиться заголовок CSP.

Если вместо этого, вы предпочитаете добавлять CSP к отдельным маршрутам или группам любых маршрутов, можете использовать middleware в файле web.php. Например, если бы мы хотели применить middleware только к определённому маршруту, то могли бы сделать что-то вроде:

use Spatie\Csp\AddCspHeaders;

Route::get('example-route', 'ExampleController')->middleware(AddCspHeaders::class);

Или, если хотите применить middleware к группе маршрутов, можете сделать следующее:

use Spatie\Csp\AddCspHeaders;

Route::middleware(AddCspHeaders::class)->group(function () {
// Здесь задаём маршруты...
});

По умолчанию, если вы явно не определяете политику, которую следует использовать middleware, будет использоваться политика, определённая в ключе default опубликованного файла конфигурации 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 () {
// Здесь задаём маршруты...
});

Использование политики безопасности контента по умолчанию

Пакет поставляется с политикой по умолчанию 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 можно определить множество правил, и они быстро могут стать очень сложными. Итак, чтобы помочь получить краткое представление рассмотрим несколько общих правил, которые вы, вероятно будет использовать в своём собственном приложении.

Для целей этого руководства мы собираемся сделать предположение, что у нас есть проект, в котором используются следующие ресурсы на странице:

Создадим политику безопасности контента, которая разрешает загрузку только указанных выше элементов на странице. Если браузер попытается загрузить какой-либо другой ресурс, запрос будет заблокирован и не будет загружен.

Базовое представление Blade для страницы может выглядеть примерно так:

<html>
<head>
<title>CSP Test</title>

{{-- Загружаем Vue.js из CDN --}}
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

{{-- Загружаем JS сценарий с нашего домена --}}
<script src="{{ asset('js/app.js') }}"></script>

{{-- Загружаем Bootstrap 5 CSS --}}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD"
crossorigin="anonymous"
>


{{-- Загружаем CSS файл с нашего домена --}}
<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">

{{-- Задаём некий JS сценарий прямо в HTML. --}}
<script>
console.log('Loaded inline script!');
</script>

{{-- Злой JS-сценарий, который написали не мы мы, а вставил другой сценарий! --}}
<script>
console.log('Внедрённый вредоносный сценарий! ☠️');
</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();

// Здесь мы можем добавить свои собственные директивы...
}
}

Как видно из комментария к приведённому выше коду, мы можем разместить свои собственные директивы в методе 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/npm/bootstrap@5.3.0-alpha1/'])
->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/vue@3/;style-src 'self' 'nonce-3fvDDho6nNJ3xXPcK3VMsgBWjVTJzijk' https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/

В примере выше мы определили, что любой JS файл, загружаемый с URL-адреса, начинающегося с https://unpkg.com/vue@3/, может быть загружен. Это означает, что сценарий Vue.js сможет загружаться, как и ожидалось.

Мы также разрешили загрузку любого CSS файла, который загружается с URL-адреса, начинающегося с https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/.

Кроме того, мы также разрешили загрузку любого изображения, полученного с URL-адреса, начинающегося с https://laravel.com.

Вам также может быть интересно, где находятся директивы, позволяющие запускать встроенный JavaScript и загружать файлы изображений, CSS и JS из нашего домена. Все они включены в базовую политику, поэтому нам не нужно добавлять их самостоятельно. Таким образом, мы можем сохранить CustomPolicy красивой и компактной и добавлять только те директивы, которые нам нужны (обычно для внешних ресурсов).

Однако на данный момент, если бы попытались запустить встроенный JavaScript, это бы не сработало. Далее мы расскажем, как это исправить.

Хотя приведённые выше правила работают и позволяют странице загружаться должным образом, вы можете сделать правила более строгими, чтобы ещё больше повысить безопасность страницы.

Давайте представим, что по какой-то неизвестной причине вредоносному сценарию удалось проникнуть на URL-адрес, начинающийся с https://unpkg.com/vue@3/, например 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/npm/bootstrap@5.3.0-alpha1/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/vue@3/dist/vue.global.js;style-src 'self' 'nonce-20gXfzoeWpjyg1ryUkWAma5gMWNN03xH' https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css

Используя описанный выше подход, мы можем значительно улучшить безопасность страницы, поскольку теперь разрешена загрузка только тех сценариев, стилей и изображений, которые нужны.

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

Добавление одноразовых номеров в CSP

Теперь, когда мы рассмотрели, как можно разрешить загрузку внешних ресурсов. Нужно взглянуть на то, как мы можем разрешить запуск встроенных сценариев.

Возможно, вы помните, что в нашем представлении Blade было два встроенных блока сценария:

Сценарий добавленный в нижнюю часть blade файла выглядел так:

<html>
<!-- ... -->

{{-- Задаём некий JS сценарий прямо в HTML. --}}
<script>
console.log('Loaded inline script!');
</script>

{{-- Злой JS-сценарий, который написали не мы мы, а вставил другой сценарий! --}}
<script>
console.log('Внедрённый вредоносный сценарий! ☠️');
</script>
</body>
</html>

Чтобы разрешить запуск встроенных сценариев, мы можем использовать одноразовые номера. nonce — это случайная строка, генерируемая для каждого запроса. Затем эта строка добавляется в заголовок CSP (добавляется через базовую политику, которую мы расширили), и любой загружаемый встроенный сценарий должен содержать этот одноразовый номер в своём атрибуте nonce.

Давайте обновим представление blade, добавив в него nonce для безопасности встроенного сценария, используя хелпер csp_nonce(), предоставленный пакетом:

<html>
<!-- ... -->

{{-- Задаём некий JS сценарий прямо в HTML. --}}
<script nonce="{{ csp_nonce() }}">
console.log('Loaded inline script!');
</script>

{{-- Злой JS-сценарий, который написали не мы мы, а вставил другой сценарий! --}}
<script>
console.log('Внедрённый вредоносный сценарий! ☠️');
</script>
</body>
</html>

В результате этого безопасный встроенный сценарий будет работать, как и ожидалось. Принимая во внимание, что внедрённый сценарий не имеет атрибута nonce, будет заблокирован от выполнения.

Использование мета тега

Маловероятно, но возможно вы обнаружите, что содержимое вашего заголовка Content-Security-Policy превышает максимально допустимую длину. Если это так, мы можем добавить на страницу мета тег, который добавляет на страницу правила для браузера.

Для этого вы можете добавить Blade директиву пакета @cspMetaTag к тегу <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/vue@3/dist/vue.global.js;style-src 'self' 'nonce-oLbaz3rNhqvzKooMU8KpnqxgO9bFG1XQ' https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css">

Советы по реализации CSP в существующем Laravel приложении

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

Поэтому, если у вас есть возможность внедрить CSP при первом запуске нового приложения, я настоятельно рекомендую сделать это. Гораздо проще написать политику одновременно со сборкой приложения. Существует меньшая вероятность того, что забудете добавить определённые правила, и вы даже можете добавить правила политики в тот же коммит git, что и ресурс, чтобы могли легко отслеживать его в будущем.

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

Во-первых, вы можете включить для своей политики режим только отчёт/report only. Это позволит определить политику, но всякий раз, когда какое-либо из правил нарушается (например, загрузка ресурса, загрузка которого не разрешена), отчёт будет предоставлен на заданный URL-адрес вместо блокировки ресурса. Это позволяет создать CSP, который вы хотели бы использовать, и протестировать его в продакшене, не нарушая работу приложения. Затем вы можете использовать отчёты, чтобы определить ресурсы, которые пропустили, и добавить их в свою политику.

Для включения отчёта политики, сначала нужно задать URL-адрес, на который будет выполняться запрос при обнаружении нарушений. Вы можете добавить его, задав поле CSP_REPORT_URI в .env файле следующим образом:

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/npm/bootstrap@5.3.0-alpha1/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/vue@3/dist/vue.global.js;style-src 'self' 'nonce-hI66wwieLS9inQh9GO4iaItVTFoPcNnj' https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css

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

Кроме того, также может быть полезно постепенно повышать строгость правил, как мы рассмотрели это ранее в статье. Таким образом, вы можете начать использовать только домены или подстановочные знаки в первоначальном CSP, а затем постепенно изменять правила, для использования более конкретных URL-адресов.

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

Заключение

Надеюсь эта статья дала обзор CSP, проблем которые они решают, и того, как они работают. Теперь вы должны знать, как реализовать CSP в Laravel приложении с помощью пакета spatie/laravel-csp.

Вы также можете ознакомиться с документацией MDN по CSP, в которой объясняется больше опций, доступных для использования в приложениях.

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

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

Кликджекинг (UI redressing)

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

Middleware обеспечивающее безопасность Laravel