Content Security Policy (CSP) - это механизм безопасности веб-приложений, который используется для сокращения рисков, связанных с атаками, такими как внедрение скриптов (XSS) и выполнение нежелательного кода (инъекция). CSP позволяет веб-разработчикам указывать браузерам, из каких источников разрешено загружать ресурсы, такие как скрипты, стили, изображения, шрифты и другие элементы.

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

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

Что дальше?

Окей, мы разобрались с тем что такое CSP, но с чего начать, а самое главное как мы будем тестировать, работает этот механизм или нет?

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

Мое приложение использует стек:

  • React.js

  • Typescript

  • Material UI

  • Styled-components

Так же есть прямой доступ к конфигу webpack с конфигурацией сборки.

При локальной разработке у меня используется сервер на node.js "start": "node scripts/start.js"

Но для нашего тестирования CSP нам потребуется настраивать заголовки на сервере, и самым популярным решением является поднять локально сервер на nginx вместо нашего скрипта.

Nginx Шаг 1

В зависимости от вашей os, команды по установке и запуску nginx могут немного отличаться. Так как я использую mac os, я устанавливал nginx через brew (https://brew.sh)

После того как мы установили nginx, у нас есть доступ к базовой конфигурации.

Для Mac Os nginx лежит по адресу /usr/local/etc/nginx либо /opt/homebrew/etc/nginx/nginx.conf базовый конфиг nginx.conf

Первое что я хотел сделать - чтобы nginx, для начала, просто отдавал мою статику.

Поэтому мой базовый конфиг для локальной работы выглядел так:


worker_processes  1;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;

    keepalive_timeout  65;

    server {
        listen       3000;
        server_name  localhost;

        location / {
            root   html;
            index  index.html index.htm;
        }


        error_page   403 404 500 502 503 504  /index.html;

        location = / {
            root   html;
        }
    }

    include servers/*;
}

В данном случае наш сервер должен отдавать статику по адресу http://localhost:3000/

Теперь можем запустить nginx командой sudo nginx

Если мы перейдем по адресу http://localhost:3000/ то должны увидеть что то вроде этого:

Nginx Шаг 2

Теперь нам нужно сбилдить нашу статику и положить ее в usr/local/var/www или /opt/homebrew/var/www

В моем проекте это делается командой yarn run build

На выходе получаем папку static, берем ее внутренности и перемещаем в usr/local/var/www

После этого перезапускаем nginx командой sudo nginx -s stop && sudo nginx

Теперь на http://localhost:3000мы должны увидеть наше приложение

P.S Этот шаг нужно повторять каждый раз, когда вы обновляете код своего приложения, если хотите проверить результат

Nginx Шаг 3

Далее будет не лишним настроить протокол https, чтобы наш сайт открывался по https://localhost:3000

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

Вводим в терминал команду openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem

После выполнения команды жмем enter чтобы скипать шаги до шага Common name.

На этом моменте для Common name вводим 127.0.0.1

чтобы установить сертификат в корневом хранилище сертификатов вашей ОС или в браузере, чтобы он был надежным.

В корне проекта появиться два файла cert.pem key.pem

Далее нам нужно перенести эти два файла в папку /usr/local/etc/ca-certificates или /opt/homebrew/etc/ca-certificates

И в конфиге nginx добавить следующие поля ниже поля server_name

P.S Опять же путь до файлов может отличаться


ssl_certificate /usr/local/etc/ca-certificates/cert.pem;

ssl_certificate_key /usr/local/etc/ca-certificates/key.pem;

Так же требуется добавить приписку ssl для поля listen

Получиться примерно так:



worker_processes  1;



events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    
    keepalive_timeout  65;

    server {
        listen       3000 ssl;
        server_name  localhost;

        
        ssl_certificate /usr/local/etc/ca-certificates/cert.pem;

        
        ssl_certificate_key /usr/local/etc/ca-certificates/key.pem;

        location / {
            root   html;
            index  index.html index.htm;
        }


        error_page   403 404 500 502 503 504  /index.html;

        location = / {
            root   html;
        }
    }

    include servers/*;
}

Перезапускаем nginx командой sudo nginx -s stop && sudo nginx

Теперь сайт должен открываться на https://localhost:3000

Nginx Шаг 4

По идее сейчас все готово для того чтобы внедрять политику CSP.

Для того чтобы ее настраивать нам нужно понимать как она работает.

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

Настройка начинается с того, что сначала мы запрещаем все, а потом начинаем добавлять исключения.

Начнем с правила script-src оно определяет допустимые источники JavaScript.

В nginx  добавляем заголовок со следующим значением:

add_header Content-Security-Policy "script-src 'self' 'unsafe-inline'";

В коде это выглядит так:



worker_processes  1;



events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    
    keepalive_timeout  65;

    server {
        listen       3000 ssl;
        server_name  localhost;

        
        ssl_certificate /usr/local/etc/ca-certificates/cert.pem;

        
        ssl_certificate_key /usr/local/etc/ca-certificates/key.pem;

        location / {
			add_header Content-Security-Policy "script-src 'self' 'unsafe-inline'";
            root   html;
            index  index.html index.htm;
        }


        error_page   403 404 500 502 503 504  /index.html;

        location = / {
            root   html;
        }
    }

    include servers/*;
}

Перезапускаем nginx командой sudo nginx -s stop && sudo nginx

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

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

add_header Content-Security-Policy "script-src 'self' 'unsafe-inline' https://www.google.com/recaptcha/";

Перезапускаем nginx командой sudo nginx -s stop && sudo nginx

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

Nginx Шаг 5

Казалось бы мы настроили первое правило script-src - но нет. В нашем правиле есть ключевое слово 'unsafe-inline' который означает что мы разрешаем использование всех встроенных скриптов. Использование этого ключевого слова считается небезопасным.

Давайте попробуем удалить это ключевое слово, и посмотреть что будет:

Высокая вероятность того что вы получите такую ошибку выше, и скорее всего подобных ошибок будет +- около 10, а то и больше, в зависимости от того сколько встроенных скриптов вы используете.

Так что же тут случилось, откуда тут вообще взялась эта ошибка?

Так как мы отказались от ключевого слово 'unsafe-inline' - то мы отказались и от использования встроенных скриптов. Теперь же нам нужно научиться “помечать” какие встроенные скрипты являются безопасными.

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

Настройка хеша, включает в себя изменение кода как со стороны frontend так и со стороны nginx.

Для начала начнем со стороны frontend. В целом, описанные ниже шаги можно применить на большинство фреймворков и библиотек, так как основные настройки делаются в корневом index.js и в webpack конфиге.

Настройка frontend приложения

Сама настройка заключается в том, что мы должны добавить атрибут nonce="CSP_NONCE" ко всем встроенным скриптам. Само значение CSP_NONCE на самом деле может быть любым. Это значение нужно для того, чтобы бы в будущем наш nginx находил это значение в статических файлах js, html и заменял на динамический хеш.

Начнем с простого, зайдем в наш index.html файл, и добавим этот атрибут ко всем подключаемым скриптам, стилям и шрифтам. К примеру у меня есть следующие скрипт и шрифт, в которые я добавляю атрибут:


<link nonce="**CSP_NONCE**" rel="preconnect" href="https://fonts.googleapis.com" />

<script nonce="**CSP_NONCE**" type="text/javascript">
    window.dataLayer=window.dataLayer||[];
</script>

Добавляем в наш index.html следующую запись

<meta property="csp-nonce" content="**CSP_NONCE**" />

На эту запись могут ориентироваться некоторые UI библиотеки, например Material UI

Так же добавляем в index.html следующий скрипт

<script type="text/javascript" nonce="**CSP_NONCE**">
    window.__webpack_nonce__ = "**CSP_NONCE**";
</script>

Тут мы глобально задаем новую переменную webpack_nonce на которую будут ориентироваться некоторые скрипты и библиотеки.

Далее открываем наш конфиг для webpack, и находим массив с плагинами plugins: []

Как правило в этом месте описаны настройки для различных плагинов eslint, html.

Нам нужно установить плагин html-webpack-inject-attributes-plugin

Подключить его вверху конфига:

И добавить следующую запись последним элементов массива plugins

plugins: [
	// ...any plugins
	new HtmlWebpackInjectPlugin({
    nonce: "**CSP_NONCE**",
	}),
]

Так как использую react, у меня есть скрипт scripts/build.js, который запускается командой yarn run build , этот файл используется для сборки приложения в production

У вас может быть точно такой же файл, либо какой то другой аналогичный скрипт. В него нужно добавить следующую запись:

process.env.INLINE_RUNTIME_CHUNK = "false";

Открываем наш корневой index.js, и в самый вверх добавляем запись:

// eslint-disable-next-line no-undef
__webpack_nonce__ = window.__webpack_nonce__;

По идее все эти шаги должны привести к тому, что каждый ваш встроенный скрипт, стиль, включая динамические, будет иметь атрибут nonce="CSP_NONCE" :

Nginx Шаг 6

После того как мы добавили nonce="CSP_NONCE" ко всем встроенным скриптам и стилям, нужно расширить конфигурацию nginx, и добавить:

Два поля в раздел location:

sub_filter_once off;

sub_filter **CSP_NONCE** $request_id;

Поле sub_filter_once указывает следует ли искать каждую строку для замены один раз.

Поле sub_filter задает строку для замены и строку замены.

То есть мы находим строку **CSP_NONCE** в нашей статике, и заменяем ее на значение переменной $request_id

Из заголовка удаляем строку unsafe-inline которая разрешала нам использование всех встроенных скриптов, и добавляем 'nonce-$request_id' и 'strict-dynamic'

'strict-dynamic' - указывает, что доверие, явно предоставляемое скрипту, присутствующему в разметке, путем сопровождения его одноразовым значением или хэшем, должно распространяться на все скрипты, загруженные этим корневым скриптом.

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

 

worker_processes  1;



events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    
    keepalive_timeout  65;

    server {
        listen       3000 ssl;
        server_name  localhost;

        
        ssl_certificate /usr/local/etc/ca-certificates/cert.pem;

        
        ssl_certificate_key /usr/local/etc/ca-certificates/key.pem;

        location / {
			add_header Content-Security-Policy "script-src 'self' 'nonce-$request_id' 'strict-dynamic' https://www.google.com/recaptcha/";
			sub_filter_once off;
            sub_filter **CSP_NONCE** $request_id;
            root   html;
            index  index.html index.htm;
        }


        error_page   403 404 500 502 503 504  /index.html;

        location = / {
            root   html;
        }
    }

    include servers/*;
}

После перезапуска nginx, у нас должны пропасть ошибки из консоли.

Nginx Шаг 7

Теперь продолжим добавлять различные директивы, например style-src со значением self

Перезапускаем nginx, обновляем страницу, и смотрим ошибки в консоли.

Скорее всего самые первые ошибки будут указывать на ресурсы, которых нет в white list.

Refused to load the stylesheet '<https://example.com>' because it violates the following Content Security Policy directive: "style-src 'self'". Note that 'style-src-elem' was not explicitly set, so 'style-src' is used as a fallback.

Добавляем ресурс и nonce:

style-src “’self’ ‘nonce-$request_id’ https://*.example.com”

 Так же для удобства, выносим каждую директиву в переменную, и получаем:

worker_processes  1;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;

    keepalive_timeout  65;

    server {
        listen       3000 ssl;
        server_name  localhost;

        ssl_certificate /opt/homebrew/etc/ca-certificates/cert.pem;

        ssl_certificate_key /opt/homebrew/etc/ca-certificates/key.pem;

        set $CSP_SCRIPT_SRC "'self' 'nonce-$request_id' 'strict-dynamic' https://www.google.com/recaptcha/";
        set $CSP_STYLE_SRC "'self' 'nonce-$request_id' https://*.example.com";

        location / {
            add_header Content-Security-Policy "script-src $CSP_SCRIPT_SRC; style-src $CSP_STYLE_SRC";
            sub_filter_once off;
            sub_filter **CSP_NONCE** $request_id;
            root   html;
            index  index.html index.htm;
        }

        error_page   500 502 503 504  /index.html;
        location = / {
            root   html;
        }
    }

    include servers/*;
}

По такому принципу, вы можете добавить остальные директивы.

У вас получиться примерно так:

#user  nobody;
worker_processes  1;



events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       3000 ssl;
        server_name  localhost;

        # location of ssl certificate
        ssl_certificate /usr/local/etc/ca-certificates/cert.pem;

        # location of ssl key
        ssl_certificate_key /usr/local/etc/ca-certificates/key.pem;

        set $CSP_SCRIPT_SRC "'self' 'nonce-$request_id' 'strict-dynamic' https://www.google.com/recaptcha/";

        set $CSP_STYLE_SRC "'self' 'nonce-$request_id' https://*.example.com";

        set $CSP_CONNECT_SRC "'self' https://*.example.com";

        set $CSP_FONT_SRC "'self' https://fonts.gstatic.com/";

        set $CSP_IMG_SRC "'self'";

        set $CSP_OBJECT_SRC "'self'";

        set $CSP_BASE_URI "'self'";

        set $CSP_FRAME_SRC "'self' https://www.google.com https://mc.yandex.ru";

        set $CSP_MANIFEST_SRC "'self'";

        set $CSP_MEDIA_SRC "'self'";

        set $CSP_WORKER_SRC "'self'";

        set $CSP_FRAME_ANCESTORS "'self'";

        location / {
            add_header Content-Security-Policy "default-src 'none'; script-src $CSP_SCRIPT_SRC; style-src $CSP_STYLE_SRC; connect-src $CSP_CONNECT_SRC; font-src $CSP_FONT_SRC; img-src $CSP_IMG_SRC; object-src $CSP_OBJECT_SRC; base-uri $CSP_BASE_URI; frame-src $CSP_FRAME_SRC; manifest-src $CSP_MANIFEST_SRC; media-src $CSP_MEDIA_SRC; worker-src $CSP_WORKER_SRC; frame-ancestors $CSP_FRAME_ANCESTORS;";
            sub_filter_once off;
            sub_filter **CSP_NONCE** $request_id;
            root   html;
            index  index.html index.htm;
        }


        error_page   403 404 500 502 503 504  /index.html;

        location = / {
            root   html;
        }
    }

    include servers/*;
}

В целом на этом моменте можно считать настройку CSP завершенной.

Комментарии (0)