• Время чтения ~12 мин
  • 20.01.2023

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

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

Что такое политика безопасности содержимого?

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

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

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

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

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

Как мы уже упоминали, CSP — это просто набор правил, которые возвращаются с вашего сервера в браузер клиента через заголовок в ответе или иногда определяются как тег <meta> в HTML. Это означает, что существует несколько способов применения 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 ко всем своим веб-маршрутам, вы можете добавить класс промежуточного ПО Spatie\Csp\AddCspHeaders в веб-часть массива $middlewareGroups в файле app/Http/Kernel.php:

// ...
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, который выглядит примерно так:

ase-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/vue@3/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/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/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 пакета. Не существует определенного каталога, в который вам нужно поместить его, поэтому вы можете выбрать место, которое лучше всего подходит для вашего приложения. Мне нравится помещать его в каталог приложения / поддержки / Csp / политик, но это только мое предпочтение. Поэтому я создам новое приложение/Support/Csp/Policies/CustomPolicy.php file:

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/npm/[email protected]/'])
            ->addDirective(Directive::IMG, 'https://laravel.com');
    }
}

Приведенная выше политика создаст заголовок Content-Security-Policy, который выглядит примерно так:

ase-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/[email protected]/

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

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

Кроме того, мы также разрешили загружать любое изображение, полученное с 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/[email protected]/dist/css/bootstrap.min.css'])
            ->addDirective(Directive::IMG, 'https://laravel.com/img/logotype.min.svg');
    }
}

Приведенная выше политика создаст заголовок Content-Security-Policy, который выглядит примерно так:

ase-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/[email protected]/dist/css/bootstrap.min.css

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

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

Добавление Nonces в ваш CSP

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

Возможно, вы помните, что у нас было два встроенных блока скриптов в нашем представлении 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.

Давайте обновим наше представление Blade, включив в него nonce для нашего безопасного встроенного скрипта с помощью помощника csp_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, будет заблокирован от выполнения.

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

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

Для этого вы можете добавить директиву @cspMetaTag Blade пакета в тег <head> вашего представления, например so:

<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/[email protected]/dist/css/bootstrap.min.css">

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

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

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

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

Во-первых, вы можете включить режим «только отчет» для своей политики. Это позволяет определить политику, но всякий раз, когда какое-либо из правил нарушается (например, загружается ресурс, который не разрешено загружать), отчет будет отправляться на заданный 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/[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. Приведенная выше политика сгенерирует заголовок, который выглядит следующим образом:

eport-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/[email protected]/dist/css/bootstrap.min.css

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

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

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

Заключение

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

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

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Об авторе CrazyBoy49z
WORK EXPERIENCE
Контакты
Ukraine, Lutsk
+380979856297