Browser Support
Межсайтовый скриптинг (XSS) – это возможность внедрять вредоносные скрипты в веб-приложения. На протяжении более чем десяти лет он остается одной из самых серьезных уязвимостей в интернете.
Content Security Policy (CSP) – это дополнительный уровень защиты, который помогает снизить риск межсайтового скриптинга. Чтобы настроить CSP, добавьте HTTP-заголовок Content-Security-Policy на веб-страницу и задайте значения, которые определяют, какие ресурсы агент пользователя может загружать для этой страницы.
В этой статье рассказывается, как использовать CSP на основе nonce-значений или хешей для защиты от XSS-атак вместо часто используемых CSP на основе белых списков хостов, которые часто оставляют страницу уязвимой для XSS-атак, поскольку их можно обойти в большинстве конфигураций.
Ключевой термин: nonce – это случайное число, которое используется только один раз и позволяет пометить тег <script> как надежный.
Ключевой термин: хеш-функция – это математическая функция, которая преобразует входное значение в сжатое числовое значение, называемое хешем. Чтобы пометить встроенный тег <script> как надежный, можно использовать хеш (например, SHA-256).
Content Security Policy на основе одноразовых кодов или хешей часто называют строгой CSP. Если в приложении используется строгая CSP, злоумышленники, обнаружившие уязвимости для HTML-инъекций, обычно не могут использовать их, чтобы заставить браузер выполнить вредоносные скрипты в уязвимом документе. Это связано с тем, что строгая CSP разрешает только хешированные скрипты или скрипты с правильным значением nonce, сгенерированным на сервере. Таким образом, злоумышленники не могут выполнить скрипт, не зная правильного значения nonce для данного ответа.
Зачем использовать строгую CSP?
Если на вашем сайте уже есть CSP, которая выглядит как script-src www.googleapis.com, то она, скорее всего, неэффективна против межсайтового скриптинга. Такой тип CSP называется белым списком CSP. Они требуют тщательной настройки, и злоумышленники могут их обойти.
Строгие правила CSP, основанные на криптографических одноразовых кодах или хешах, позволяют избежать этих проблем.
Структура строгих CSP
Базовые строгие правила Content Security Policy используют один из следующих заголовков HTTP-ответа:
Строгая CSP на основе одноразового кода
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
Строгая CSP на основе хеша
Content-Security-Policy:
script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
Следующие свойства делают CSP строгой и, следовательно, безопасной:
- Для этого используются одноразовые коды
'nonce-{RANDOM}'или хеши'sha256-{HASHED_INLINE_SCRIPT}', которые указывают, какие теги<script>разработчик сайта считает надежными для выполнения в браузере пользователя. - Она задает
'strict-dynamic', чтобы упростить развертывание CSP на основе одноразового кода или хеша, автоматически разрешая выполнение скриптов, созданных доверенным скриптом. Это также позволяет использовать большинство сторонних библиотек и виджетов JavaScript. - Она не основана на белых списках URL, поэтому не имеет недостатков, присущих обычным CSP.
- Она блокирует ненадежные встроенные скрипты, например встроенные обработчики событий или URI
javascript:. - Она ограничивает
object-src, чтобы отключить опасные плагины, такие как Flash. - Она ограничивает
base-uri, чтобы заблокировать внедрение тегов<base>. Это не позволяет злоумышленникам менять местоположение скриптов, загружаемых из относительных URL.
Используйте строгие CSP
Чтобы внедрить строгую CSP, выполните следующие действия:
- Решите, какую политику CSP вы будете использовать: на основе nonce-значений или хешей.
- Скопируйте CSP из раздела Строгая структура CSP и задайте его в качестве заголовка ответа в своем приложении.
- Проведите рефакторинг шаблонов HTML и клиентского кода, чтобы удалить шаблоны, несовместимые с CSP.
- Разверните CSP.
Чтобы проверить, есть ли на сайте CSP и достаточно ли она строгая, чтобы защитить от XSS, можно использовать аудит Рекомендации в Lighthouse (версия 7.3.0 и выше с флагом --preset=experimental).
Шаг 1. Решите, нужна ли вам CSP на основе nonce-значений или хешей
Вот как работают два типа строгих CSP:
CSP на основе одноразового кода
При использовании CSP на основе однократно используемого числа вы генерируете случайное число во время выполнения, включаете его в CSP и связываете с каждым тегом script на странице. Злоумышленник не сможет включить или запустить вредоносный скрипт на вашей странице, потому что ему нужно будет угадать правильное случайное число для этого скрипта. Это работает, только если число нельзя угадать и оно генерируется заново для каждого ответа.
Используйте CSP на основе одноразового кода для HTML-страниц, обрабатываемых на сервере. Для таких страниц можно создавать новое случайное число для каждого ответа.
CSP на основе хеша
Для CSP на основе хеша хеш каждого тега встроенного скрипта добавляется в CSP. У каждого скрипта свой хеш. Злоумышленник не сможет включить или запустить вредоносный скрипт на вашей странице, поскольку для его запуска хеш этого скрипта должен находиться в вашем CSP.
Используйте CSP на основе хеша для статических HTML-страниц или страниц, которые нужно кешировать. Например, CSP на основе хеша можно использовать для одностраничных веб-приложений, созданных с помощью таких фреймворков, как Angular, React и т. д., которые обслуживаются статически без отрисовки на стороне сервера.
Шаг 2. Настройте строгую CSP и подготовьте скрипты
При настройке CSP у вас есть несколько вариантов:
- Режим только отчетов (
Content-Security-Policy-Report-Only) или режим принудительного применения (Content-Security-Policy). В режиме только отчетов CSP не блокирует ресурсы, поэтому на сайте ничего не ломается, но вы можете видеть ошибки и получать отчеты обо всем, что было бы заблокировано. При локальной настройке CSP это не имеет значения, поскольку в обоих режимах ошибки отображаются в консоли браузера. Режим принудительного применения может помочь вам найти ресурсы, которые блокирует ваш проект CSP, поскольку блокировка ресурса может привести к тому, что ваша страница будет выглядеть неработающей. Режим "Только отчеты" будет наиболее полезен на более поздних этапах (см. шаг 5). - Заголовок или тег HTML
<meta>. При локальной разработке тег<meta>может быть более удобным для настройки CSP и быстрого просмотра того, как он влияет на ваш сайт. Однако:- Позже, когда вы будете развертывать CSP в рабочей среде, мы рекомендуем задать ее в качестве HTTP-заголовка.
- Если вы хотите настроить CSP в режиме только для отчетов, вам нужно будет задать его в качестве заголовка, поскольку метатеги CSP не поддерживают режим только для отчетов.
Задайте в приложении следующий заголовок HTTP-ответа Content-Security-Policy:
Content-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none';
Как сгенерировать однократно используемое число для CSP
Nonce – это случайное число, которое используется только один раз при загрузке страницы. CSP на основе одноразового кода может предотвратить XSS-атаки, только если злоумышленники не могут угадать значение кода. Одноразовый код CSP должен:
- Криптографически стойкое случайное значение (желательно длиной не менее 128 бит).
- Генерируется заново для каждого ответа
- Закодировано по стандарту Base64
Ниже приведены примеры того, как добавить nonce CSP в серверные фреймворки.
- Django (Python)
- Express (JavaScript):
const app = express(); app.get('/', function(request, response) { // Generate a new random nonce value for every response. const nonce = crypto.randomBytes(16).toString("base64"); // Set the strict nonce-based CSP response header const csp = `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`; response.set("Content-Security-Policy", csp); // Every <script> tag in your application should set the `nonce` attribute to this value. response.render(template, { nonce: nonce }); });
Добавьте атрибут nonce к элементам <script>
При использовании CSP на основе одноразового кода каждый элемент <script> должен иметь атрибут nonce, соответствующий случайному значению одноразового кода, указанному в заголовке CSP. У всех скриптов может быть один и тот же одноразовый код. Первый шаг – добавить эти атрибуты во все скрипты, чтобы CSP разрешил их.
Задайте в приложении следующий заголовок HTTP-ответа Content-Security-Policy:
Content-Security-Policy: script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic'; object-src 'none'; base-uri 'none';
Для нескольких встроенных скриптов синтаксис выглядит следующим образом:
'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'.
Динамическая загрузка скриптов из источников
Сторонние скрипты можно загружать динамически с помощью встроенного скрипта.
<script>
var scripts = [ 'https://example.org/foo.js', 'https://example.org/bar.js'];
scripts.forEach(function(scriptUrl) {
var s = document.createElement('script');
s.src = scriptUrl;
s.async = false; // to preserve execution order
document.head.appendChild(s);
});
</script>{HASHED_INLINE_SCRIPT}. Чтобы уменьшить количество хешей, можно объединить все встроенные скрипты в один. Чтобы посмотреть, как это работает, ознакомьтесь с примером и его кодом.
<script src="https://example.org/foo.js"></script> <script src="https://example.org/bar.js"></script>
integrity, соответствующего разрешенному источнику.
Примечания к загрузке скрипта
В примере встроенного скрипта добавляется s.async = false, чтобы foo выполнялся до bar, даже если bar загружается первым. В этом фрагменте s.async = false не блокирует синтаксический анализатор во время загрузки скриптов, поскольку они добавляются динамически. Синтаксический анализатор останавливается только на время выполнения скриптов, как и в случае со скриптами async. Однако при использовании этого фрагмента кода необходимо учитывать следующее:
-
Один или оба скрипта могут выполниться до того, как документ будет полностью загружен. Если вы хотите, чтобы документ был готов к моменту выполнения скриптов, дождитесь события
DOMContentLoaded, прежде чем добавлять скрипты. Если это приводит к проблемам производительности, поскольку скачивание скриптов начинается слишком поздно, используйте теги предварительной загрузки в начале страницы. -
defer = trueничего не делает. Если вам нужно, чтобы скрипт выполнялся в определенное время, запускайте его вручную.
Шаг 3. Проведите рефакторинг HTML-шаблонов и клиентского кода
Для выполнения скриптов можно использовать встроенные обработчики событий (например, onclick="…", onerror="…") и URI JavaScript (<a href="javascript:…">). Это означает, что злоумышленник, обнаруживший ошибку XSS, может внедрить такой HTML-код и выполнить вредоносный JavaScript. Политика CSP на основе nonce-значений или хешей запрещает использование такой разметки.
Если на вашем сайте используются такие шаблоны, вам нужно будет заменить их на более безопасные альтернативы.
Если вы включили CSP на предыдущем шаге, то каждый раз, когда CSP блокирует несовместимый шаблон, в консоли будут появляться сообщения о нарушениях.
В большинстве случаев исправить ошибку несложно:
Рефакторинг встроенных обработчиков событий
<span id="things">A thing.</span>
<script nonce="${nonce}">
document.getElementById('things').addEventListener('click', doThings);
</script><span onclick="doThings();">A thing.</span>
Рефакторинг javascript: URI
<a id="foo">foo</a>
<script nonce="${nonce}">
document.getElementById('foo').addEventListener('click', linkClicked);
</script><a href="javascript:linkClicked()">foo</a>
Удалите eval() из кода JavaScript
Если ваше приложение использует eval() для преобразования сериализаций строк JSON в объекты JS, вам следует провести рефакторинг таких экземпляров в JSON.parse(), что также быстрее.
Если вы не можете удалить все случаи использования eval(), вы все равно можете задать строгую CSP на основе одноразового кода, но вам придется использовать ключевое слово CSP 'unsafe-eval', что делает вашу политику немного менее безопасной.
Эти и другие примеры рефакторинга можно найти в практической работе по строгой CSP:
Шаг 4 (необязательно). Добавьте резервные объявления для поддержки старых версий браузеров
Browser Support
Если вам нужно обеспечить поддержку устаревших браузеров, выполните следующие действия:
- При использовании
strict-dynamicнеобходимо добавитьhttps:в качестве резервного варианта для более ранних версий Safari. После этого:- Все браузеры, поддерживающие
strict-dynamic, игнорируют резервную директивуhttps:, поэтому она не снижает эффективность правил. - В старых браузерах скрипты из внешних источников могут загружаться, только если они поступают из источника HTTPS. Это менее безопасно, чем строгая CSP, но все же предотвращает некоторые распространенные причины XSS, такие как внедрение URI
javascript:.
- Все браузеры, поддерживающие
- Чтобы обеспечить совместимость с очень старыми версиями браузеров (выпущенными более четырех лет назад), можно добавить резервную директиву
unsafe-inline. Все современные браузеры игнорируютunsafe-inline, если присутствует однократно используемое число или хеш CSP.
Content-Security-Policy:
script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none';
base-uri 'none';
Шаг 5. Разверните CSP
Убедившись, что CSP не блокирует допустимые скрипты в локальной среде разработки, вы можете развернуть CSP в промежуточной, а затем и в рабочей среде.
- При необходимости разверните CSP в режиме только для отчетов, используя заголовок
Content-Security-Policy-Report-Only. Режим только для отчетов удобен для тестирования потенциально критических изменений, таких как новый CSP в рабочей среде, прежде чем вы начнете применять ограничения CSP. В режиме только для отчетов CSP не влияет на поведение приложения, но браузер по-прежнему генерирует ошибки консоли и отчеты о нарушениях, когда сталкивается с шаблонами, несовместимыми с CSP. Это позволяет увидеть, что могло бы не работать у конечных пользователей. Подробнее о Reporting API… - Когда вы убедитесь, что CSP не нарушает работу сайта для конечных пользователей, разверните CSP с помощью заголовка ответа
Content-Security-Policy. Рекомендуем задавать CSP с помощью HTTP-заголовка на стороне сервера, поскольку это безопаснее, чем использовать тег<meta>. После этого CSP начнет защищать ваше приложение от XSS-атак.
Ограничения
Строгая политика безопасности контента обычно обеспечивает надежный дополнительный уровень защиты, который помогает снизить риск межсайтового скриптинга. В большинстве случаев CSP значительно сокращает направления атак, отклоняя опасные шаблоны, такие как URI javascript:. Однако в зависимости от типа используемой политики CSP (nonce-значения, хеши, с директивой 'strict-dynamic' или без нее) в некоторых случаях она не обеспечивает достаточную защиту приложения.
- Если вы используете nonce для скрипта, но в тело или параметр
srcэлемента<script>внедряется код, - Если в места динамически создаваемых скриптов (
document.createElement('script')) внедряется код, в том числе в любые библиотечные функции, которые создают узлы DOMscriptна основе значений своих аргументов. К ним относятся некоторые распространенные API, такие как.html()в jQuery, а также.get()и.post()в jQuery < 3.0. - Если в старых приложениях AngularJS есть внедрения шаблонов. Злоумышленник, который может внедрить код в шаблон AngularJS, может использовать его для выполнения произвольного кода JavaScript.
- Если правило содержит
'unsafe-eval', внедрения вeval(),setTimeout()и некоторые другие редко используемые API.
Разработчикам и инженерам по безопасности следует обращать особое внимание на такие шаблоны при код-ревью и аудите безопасности. Подробную информацию об этих случаях можно найти в статье Content Security Policy: A Successful Mess Between Hardening and Mitigation (Content Security Policy: успешный компромисс между усилением защиты и устранением уязвимостей).
Дополнительные сведения
- CSP Is Dead, Long Live CSP! On the Insecurity of Whitelists and the Future of Content Security Policy
- CSP Evaluator
- LocoMoco Conference: Content Security Policy - A successful mess between hardening and mitigation
- Выступление на Google I/O: защита веб-приложений с помощью современных функций платформы