Предотвращение межсайтового скриптинга (XSS) при помощи строгой политики безопасности контента (CSP)
Развертывание CSP-политики на основе одноразовых номеров или хешей скриптов для обеспечения комплексной защиты от межсайтового скриптинга.
Зачем нужна строгая политика безопасности контента (CSP)? #
Межсайтовый скриптинг (XSS) — внедрение в веб-приложения вредоносных скриптов — уже более десяти лет является одной из самых серьезных уязвимостей в области веб-безопасности.
Политика безопасности контента (CSP) — это дополнительная мера защиты, нацеленная на предотвращение XSS-атак. Для настройки CSP-политики необходимо добавить на веб-страницу HTTP-заголовок Content-Security-Policy и установить значения, чтобы указать, какие ресурсы пользовательскому агенту разрешено загружать в рамках этой страницы. Эта статья рассказывает, как защититься от XSS-атак, используя CSP-политику на основе одноразовых номеров или хешей в противоположность стандартным CSP-политикам на основе белого списка доменов, которые зачастую оставляют страницу уязвимой для XSS-атак, поскольку в большинстве конфигураций их можно обойти.
Политика безопасности контента, основанная на использовании одноразовых номеров или хешей, часто называется строгой CSP-политикой. Если приложение использует строгую CSP-политику, злоумышленники, обнаруживающие неправильно внедренный HTML-код, в большинстве случаев не смогут с его помощью заставить браузер выполнять вредоносные скрипты в контексте уязвимого документа. Это обусловлено тем, что при использовании строгой CSP-политики разрешается выполнение только хешированных скриптов или скриптов с верно указанным одноразовым номером, сгенерированным на сервере, благодаря чему злоумышленник не может выполнить скрипт, так как не имеет одноразового номера, соответствующего ответу.
Почему строгая CSP-политика предпочтительнее CSP-политики на основе белого списка #
Если на вашем сайте применяется CSP-политика с явным указанием домена (script-src www.googleapis.com), имейте в виду, что она может не быть эффективным средством защиты от межсайтового скриптинга. Такая CSP-политика называется политикой на основе белого списка, и у нее есть два недостатка:
- Она требует существенного изменения кода.
- В большинстве вариантов конфигурации ее можно обойти.
Эти недостатки делают CSP на основе белого списка неэффективным средством защиты от XSS-атак. Вот почему рекомендуется использовать строгую CSP-политику на основе криптографически сгенерированных одноразовых номеров или хешей, чтобы избежать проблем, описанных выше.
CSP-политика на основе белого списка
— Не является эффективным средством защиты сайта. ❌ — Требует существенного изменения кода. 😓Строгая CSP-политика
- Эффективно защищает ваш сайт. ✅
- Код всегда имеет одинаковую структуру. 😌
В чем заключается суть строгой политики безопасности контента? #
Строгая политика безопасности контента настраивается в соответствии со структурой, показанной ниже, и для ее активации необходимо установить один из следующих заголовков ответа 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-политики ваше приложение будет использовать: на основе одноразовых номеров или на основе хешей.
- Скопировать код CSP-политики из раздела В чем заключается суть строгой политики безопасности контента и установить его в качестве заголовка ответа на всех страницах вашего приложения.
- Провести рефакторинг HTML-шаблонов и кода, выполняемого на стороне клиента, чтобы исключить несовместимые с CSP паттерны.
- Добавить резервные политики для поддержки Safari и устаревших браузеров.
- Развернуть CSP-политику.
В ходе этого процесса вы можете использовать проверку Best Practices в Lighthouse (v7.3.0 или выше с флагом --preset=experimental) для подтверждения того, что на вашем сайте действует CSP-политика и она является достаточно строгой для предотвращения XSS-атак.
Шаг 1. Определитесь, какой вид CSP использовать: на основе одноразовых номеров или на основе хешей #
Существует два вида строгих CSP-политик: на основе одноразовых номеров и на основе хешей. Вот как они работают:
- CSP-политика на основе одноразовых номеров: во время выполнения генерируется случайное число, которое указывается в коде CSP-политики и связывается с каждым тегом script на странице. Злоумышленник не сможет подключить к странице вредоносный скрипт и выполнить его, поскольку для этого ему пришлось бы угадать случайное число, соответствующее этому скрипту. Этот подход работает только в том случае, если число невозможно угадать и оно заново генерируется для каждого ответа на этапе выполнения.
- CSP-политика на основе хешей: в коде CSP-политики указывается хеш каждого встроенного тега script. Обратите внимание, что каждый скрипт имеет собственный хеш. Злоумышленник не сможет подключить к странице вредоносный скрипт и выполнить его, так как хеш скрипта должен присутствовать в коде CSP-политики.
Ниже приведены критерии для выбора типа строгой CSP-политики:
| CSP-политика на основе одноразовых номеров | Подходит для HTML-страниц, рендеринг которых происходит на сервере, благодаря чему случайный токен (одноразовый номер) можно генерировать заново для каждого ответа. |
|---|---|
| CSP-политика на основе хешей | Подходит для HTML-страниц, загружаемых статически или с использованием кеша. Пример — одностраничные веб-приложения, построенные на таких фреймворках, как Angular, React и т. д., использующие статическую загрузку без рендеринга на стороне сервера. |
Шаг 2. Установите строгую CSP-политику и подготовьте теги script #
При установке CSP-политики необходимо определиться с парой моментов:
- Какой режим использовать: режим отправки отчетов (
Content-Security-Policy-Report-Only) или режим исполнения (Content-Security-Policy). В режиме отправки отчетов CSP-политика не будет использоваться для блокирования ресурсов (т. е. все скрипты продолжат работать), однако вы сможете видеть ошибки и получать отчеты о ресурсах, попадающих под блокировку. Если вы настраиваете CSP-политику локально, между этими режимами нет существенной разницы: в обоих случаях вы будете видеть сообщения об ошибках в консоли браузера. В режиме исполнения выявлять заблокированные ресурсы и корректировать CSP-политику будет даже проще, так как вы сразу увидите, что страница функционирует неправильно. Наиболее полезным режим отправки отчетов становится на более поздних этапах процесса (см. шаг 5). - Где разместить политику: в заголовке или в HTML-теге
<meta>. При локальной разработке использование тега<meta>может быть более уместным, так как позволяет с легкостью подстраивать CSP-политику и сразу же видеть результат. Однако имейте в виду следующее:- В дальнейшем, при развертывании CSP-политики на реальном сайте, рекомендуется использовать для ее установки HTTP-заголовок.
- Если вы хотите использовать CSP-политику в режиме отправки отчетов, ее необходимо устанавливать при помощи заголовка: при установке CSP-политики при помощи тега meta режим отправки отчетов не поддерживается.
Вариант А. CSP-политика на основе одноразовых номеров
Задайте для своего приложения следующий заголовок HTTP-ответа Content-Security-Policy:
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
Генерация одноразовых номеров для CSP-политики #
Одноразовый номер — это случайное число, меняющееся при каждой загрузке страницы. CSP-политика на основе одноразовых номеров предотвращает XSS-атаки только в том случае, если значение номера не может быть угадано злоумышленником. Одноразовые номера, используемые в рамках CSP-политики, должны быть:
- Криптографически стойкими случайными числами (в идеале длиной от 128 бит).
- Генерируемыми индивидуально для каждого ответа.
- Закодированными при помощи Base64.
Вот примеры того, как указывать одноразовые номера для CSP-политики при использовании некоторых серверных фреймворков:
- Django (Python).
- Express (JavaScript):
const app = express();
app.get('/', function(request, response) {
// Генерация случайного одноразового числа заново для каждого ответа.
const nonce = crypto.randomBytes(16).toString("base64");
// Установка строгой CSP-политики на основе одноразовых чисел при помощи заголовка ответа
const csp = `script-src 'nonce-${nonce}' 'strict-dynamic' https:; object-src 'none'; base-uri 'none';`;
response.set("Content-Security-Policy", csp);
// Это значение необходимо присвоить атрибуту `nonce` для каждого тега <script> в вашем приложении.
response.render(template, { nonce: nonce });
});
}
Добавление атрибута nonce к элементам <script> #
При использовании CSP-политики на основе одноразовых номеров у каждого элемента <script> должен быть атрибут nonce, значение которого должно совпадать со случайным одноразовым номером, указанным в заголовке CSP-политики (все скрипты могут использовать один и тот же одноразовый номер). Для начала добавьте этот атрибут ко всем тегам script:
Запрещено CSP-политикой
<script src="/path/to/script.js"></script>
<script>foo()</script>
Разрешено CSP-политикой
<script nonce="${NONCE}" src="/path/to/script.js"></script>
<script nonce="${NONCE}">foo()</script>
Вариант Б. Заголовок ответа для 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}' .
Динамическая загрузка внешних скриптов #
Скрипты, подключаемые из внешних источников, необходимо загружать динамически при помощи встроенного скрипта, поскольку использование CSP-политики для проверки хешей внешних скриптов поддерживается не во всех браузерах.
Запрещено CSP-политикой
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
Разрешено CSP-политикой
<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>
Примечание об использовании async = false при загрузке скриптов: в данном случае async = false не блокирует поток, но использовать его следует с осторожностью.
В приведенном выше фрагменте кода использование s.async = false гарантирует, что foo выполнится раньше, чем bar (даже если bar загрузится первым). В данном фрагменте использование s.async = false не блокирует парсер во время загрузки скриптов; это обусловлено тем, что скрипты добавляются динамически. Приостановка парсера будет происходить только во время выполнения скриптов — точно так же, как если бы параметр async был включен. Однако при рассмотрении этого фрагмента кода учитывайте следующее:
- Выполнение скриптов может начаться до того, как документ полностью загрузится. Если вы хотите, чтобы к моменту начала выполнения скриптов документ был уже загружен, необходимо дождаться события
DOMContentLoadedи уже после этого подключить скрипты. Если это приведет к падению производительности (из-за слишком позднего начала скачивания скриптов), вы можете указать теги предварительной загрузки ближе к началу страницы. - Не используйте параметр
defer = true, поскольку он не даст нужного эффекта. Вместо этого вручную запускайте выполнение скрипта тогда, когда это требуется.
Шаг 3. Проведите рефакторинг HTML-шаблонов и кода, выполняемого на стороне клиента, чтобы исключить несовместимые с CSP паттерны #
Встроенные обработчики событий (такие, как onclick="…" или onerror="…") и URI, содержащие JavaScript (<a href="javascript:…">), могут использоваться для запуска скриптов. Это означает, что злоумышленник, обнаруживший XSS-уязвимость, сможет внедрить на страницу такой HTML-код и с его помощью выполнить вредоносный код JavaScript. CSP-политики на основе хешей и одноразовых номеров запрещают использование такой разметки. Если на вашем сайте используются описанные выше паттерны, необходимо провести рефакторинг и заменить их на более безопасные альтернативы.
Если в ходе предыдущего шага вы включили CSP, вы сможете видеть в консоли сообщения о нарушении CSP-политики каждый раз, когда происходит блокировка запрещенного паттерна.
В большинстве случаев исправление не потребует больших усилий:
Перепишите встроенные обработчики JavaScript таким образом, чтобы их добавление происходило из блока JavaScript #
Запрещено CSP-политикой
<span onclick="doThings();">A thing.</span>
Разрешено CSP-политикой
<span id="things">A thing.</span>
<script nonce="${nonce}">
document.getElementById('things')
.addEventListener('click', doThings);
</script>
Для URI с javascript: можно использовать аналогичный паттерн #
Запрещено CSP-политикой
<a href="javascript:linkClicked()">foo</a>
Разрешено CSP-политикой
<a id="foo">foo</a>
<script nonce="${nonce}">
document.getElementById('foo')
.addEventListener('click', linkClicked);
</script>
Использование eval() в JavaScript-коде #
Если ваше приложение использует eval() для преобразования данных, сериализованных в виде JSON, в объекты JS, необходимо переписать такой код с использованием JSON.parse(): это не только безопаснее, но и быстрее.
Если полностью исключить использование eval() нет возможности, вы по-прежнему сможете использовать строгую CSP-политику на основе одноразовых номеров, однако вам придется добавить параметр 'unsafe-eval', который сделает политику немного менее безопасной.
С примерами такого рефакторинга вы можете ознакомиться в интерактивном уроке, посвященном строгим CSP-политикам:
Шаг 4. Добавьте резервные политики для поддержки Safari и устаревших браузеров #
CSP-политики поддерживаются всеми основными браузерами, однако вам понадобится добавить две резервные политики:
-
При использовании
'strict-dynamic'необходимо указатьhttps:в качестве резервной политики для поддержки Safari — единственного крупного браузера, не поддерживающего'strict-dynamic'. При использовании такой конфигурации:- Все браузеры, поддерживающие
'strict-dynamic', будут игнорироватьhttps:, так что безопасность политики не снизится. - В Safari загрузка скриптов из внешних источников будет разрешена только для источников с протоколом HTTPS. Такая политика менее безопасна, чем строгая CSP-политика (поэтому она и является резервной), однако она по-прежнему защищает от многих распространенных причин XSS-атак, таких как внедрение URI с
javascript:, поскольку при наличии хеша или одноразового номера параметр'unsafe-inline'игнорируется.
- Все браузеры, поддерживающие
-
Чтобы обеспечить совместимость с очень старыми версиями браузеров (вышедшими более 4 лет назад), вы можете добавить в качестве резервной политики
'unsafe-inline'. Во всех современных браузерах при наличии CSP-политики на основе одноразового номера или хеша параметр'unsafe-inline'будет игнорироваться.
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(статья о Reporting API). Режим отправки отчетов удобен для безопасного тестирования в производственной среде изменений, которые могут потенциально нарушить работу сайта, таких как новая CSP-политика. В режиме отправки отчетов CSP-политика не влияет на поведение приложения: сайт продолжает работать так же, как и до этого. Однако браузер будет отображать в консоли сообщения и отчеты о нарушении CSP-политики, когда встречает несовместимый с ней код (и таким образом позволит диагностировать проблемы, которые возникли бы у конечных пользователей). - Удостоверившись, что CSP-политика не вызовет нарушений в работе сайта у конечных пользователей, вы можете развернуть ее, используя заголовок ответа
Content-Security-Policy. Только после выполнения этого шага CSP-политика начнет защищать ваше приложение от XSS-атак. Установка CSP-политики при помощи серверного HTTP-заголовка является более безопасной, чем использование тега<meta>; если у вас есть возможность, используйте заголовок.
Ограничения #
Как правило, строгая CSP-политика обеспечивает мощный дополнительный уровень защиты от XSS-атак. В большинстве случаев использование CSP-политики значительно снижает число уязвимых мест (полностью исключая использование таких опасных возможностей, как URI с javascript:). Однако в зависимости от типа применяемой CSP-политики (на основе одноразовых номеров, хешей, а также с использованием 'strict-dynamic' или без него) существует ряд случаев, на которые ее действие не распространяется:
- Внедрение кода непосредственно в тело или в параметр
srcэлемента<script>, защищенного одноразовым номером. - Внедрение кода в места расположения динамически создаваемых скриптов (
document.createElement('script')), включая библиотечные функции, создающие DOM-узлыscriptна основе передаваемых им аргументов. К таким функциям относятся некоторые распространенные API, такие как.html()в jQuery, а также.get()и.post()в jQuery < 3.0. - Внедрение шаблонов в старые приложения на AngularJS. Злоумышленник, внедривший шаблон AngularJS, может использовать его для выполнения произвольного JavaScript-кода.
- Внедрение кода в
eval(),setTimeout()и ряд других редко используемых API, если политика содержит параметр'unsafe-eval'.
Разработчикам и инженерам по безопасности следует обращать на такие моменты особое внимание в ходе проверок кода и аудитов безопасности. Подробнее об описанных выше случаях можно узнать в этой презентации, посвященной CSP.
Материалы для дальнейшего чтения #
- CSP Is Dead, Long Live CSP! On the Insecurity of Whitelists and the Future of Content Security Policy
- Инструмент CSP Evaluator
- Content Security Policy — A successful mess between hardening and mitigation (конференция LocoMoco)
- Securing Web Apps with Modern Platform Features (лекция с Google I/O)