Предотвращение межсайтового скриптинга (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-политика, приведенная выше, является «строгой» (и, следовательно, безопасной) по следующим причинам: Она позволяет разработчику сайта при помощи одноразовых номеров ( Она устанавливает параметр Она не основана на списках разрешенных URL-адресов и поэтому не подвержена стандартным методам обхода CSP. Она блокирует ненадежные встроенные скрипты, включая встроенные обработчики событий и URI с Она ограничивает использование Она ограничивает использование Чтобы перейти на использование строгой CSP-политики, вам необходимо: В ходе этого процесса вы можете использовать проверку Best Practices в Lighthouse (v7.3.0 или выше с флагом Существует два вида строгих CSP-политик: на основе одноразовых номеров и на основе хешей. Вот как они работают: Ниже приведены критерии для выбора типа строгой CSP-политики: При установке CSP-политики необходимо определиться с парой моментов: Задайте для своего приложения следующий заголовок HTTP-ответа Одноразовый номер — это случайное число, меняющееся при каждой загрузке страницы. CSP-политика на основе одноразовых номеров предотвращает XSS-атаки только в том случае, если значение номера не может быть угадано злоумышленником. Одноразовые номера, используемые в рамках CSP-политики, должны быть: Вот примеры того, как указывать одноразовые номера для CSP-политики при использовании некоторых серверных фреймворков: При использовании CSP-политики на основе одноразовых номеров у каждого элемента Запрещено CSP-политикой Разрешено CSP-политикой Задайте для своего приложения следующий заголовок HTTP-ответа При необходимости указать несколько встроенных скриптов используйте следующий синтаксис: Скрипты, подключаемые из внешних источников, необходимо загружать динамически при помощи встроенного скрипта, поскольку использование CSP-политики для проверки хешей внешних скриптов поддерживается не во всех браузерах. Запрещено CSP-политикой Разрешено CSP-политикой В приведенном выше фрагменте кода использование Встроенные обработчики событий (такие, как Если в ходе предыдущего шага вы включили CSP, вы сможете видеть в консоли сообщения о нарушении CSP-политики каждый раз, когда происходит блокировка запрещенного паттерна. В большинстве случаев исправление не потребует больших усилий: Запрещено CSP-политикой Разрешено CSP-политикой Запрещено CSP-политикой Разрешено CSP-политикой Если ваше приложение использует Если полностью исключить использование С примерами такого рефакторинга вы можете ознакомиться в интерактивном уроке, посвященном строгим CSP-политикам: CSP-политики поддерживаются всеми основными браузерами, однако вам понадобится добавить две резервные политики: При использовании Чтобы обеспечить совместимость с очень старыми версиями браузеров (вышедшими более 4 лет назад), вы можете добавить в качестве резервной политики Протестировав сайт в локальной среде разработки и убедившись, что CSP-политика не блокирует нужные скрипты, вы можете приступить к развертыванию CSP-политики сначала в промежуточной, а затем и в производственной среде: Как правило, строгая CSP-политика обеспечивает мощный дополнительный уровень защиты от XSS-атак. В большинстве случаев использование CSP-политики значительно снижает число уязвимых мест (полностью исключая использование таких опасных возможностей, как URI с Разработчикам и инженерам по безопасности следует обращать на такие моменты особое внимание в ходе проверок кода и аудитов безопасности. Подробнее об описанных выше случаях можно узнать в этой презентации, посвященной CSP.В чем заключается суть строгой политики безопасности контента? #
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none';Content-Security-Policy:
script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
object-src 'none';
base-uri 'none';'nonce-{RANDOM}'
) или хешей ('sha256-{HASHED_INLINE_SCRIPT}'
) помечать «доверенные» теги <script>
, выполнение которых должно быть разрешено в браузере пользователя.'strict-dynamic'
, автоматически разрешающий выполнение скриптов, создаваемых уже доверенными скриптами, что позволяет упростить внедрение CSP-политики на основе одноразовых номеров или хешей. Это также делает возможным использование большинства сторонних библиотек и виджетов на основе JavaScript.javascript:
.object-src
и тем самым блокирует использование небезопасных плагинов, таких как Flash.base-uri
, тем самым запрещая внедрение тегов <base>
, чтобы не позволить злоумышленникам изменять расположение скриптов, загружаемых при помощи относительных URL-адресов.Переход на строгую CSP-политику #
--preset=experimental
) для подтверждения того, что на вашем сайте действует CSP-политика и она является достаточно строгой для предотвращения XSS-атак.Шаг 1. Определитесь, какой вид CSP использовать: на основе одноразовых номеров или на основе хешей #
CSP-политика на основе одноразовых номеров Подходит для HTML-страниц, рендеринг которых происходит на сервере, благодаря чему случайный токен (одноразовый номер) можно генерировать заново для каждого ответа. CSP-политика на основе хешей Подходит для HTML-страниц, загружаемых статически или с использованием кеша. Пример — одностраничные веб-приложения, построенные на таких фреймворках, как Angular, React и т. д., использующие статическую загрузку без рендеринга на стороне сервера. Шаг 2. Установите строгую CSP-политику и подготовьте теги script #
Content-Security-Policy-Report-Only
) или режим исполнения (Content-Security-Policy
). В режиме отправки отчетов CSP-политика не будет использоваться для блокирования ресурсов (т. е. все скрипты продолжат работать), однако вы сможете видеть ошибки и получать отчеты о ресурсах, попадающих под блокировку. Если вы настраиваете CSP-политику локально, между этими режимами нет существенной разницы: в обоих случаях вы будете видеть сообщения об ошибках в консоли браузера. В режиме исполнения выявлять заблокированные ресурсы и корректировать CSP-политику будет даже проще, так как вы сразу увидите, что страница функционирует неправильно. Наиболее полезным режим отправки отчетов становится на более поздних этапах процесса (см. шаг 5).<meta>
. При локальной разработке использование тега <meta>
может быть более уместным, так как позволяет с легкостью подстраивать CSP-политику и сразу же видеть результат. Однако имейте в виду следующее: Вариант А. CSP-политика на основе одноразовых номеров
Content-Security-Policy
:Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none';Генерация одноразовых номеров для CSP-политики #
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>
#<script>
должен быть атрибут nonce
, значение которого должно совпадать со случайным одноразовым номером, указанным в заголовке CSP-политики (все скрипты могут использовать один и тот же одноразовый номер). Для начала добавьте этот атрибут ко всем тегам script:<script src="/path/to/script.js"></script>
<script>foo()</script><script nonce="${NONCE}" src="/path/to/script.js"></script>
<script nonce="${NONCE}">foo()</script>Вариант Б. Заголовок ответа для CSP-политики на основе хешей
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 src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script><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-политики на основе хешей и одноразовых номеров запрещают использование такой разметки. Если на вашем сайте используются описанные выше паттерны, необходимо провести рефакторинг и заменить их на более безопасные альтернативы.
Перепишите встроенные обработчики JavaScript таким образом, чтобы их добавление происходило из блока JavaScript #
<span onclick="doThings();">A thing.</span>
<span id="things">A thing.</span>
<script nonce="${nonce}">
document.getElementById('things')
.addEventListener('click', doThings);
</script>Для URI с
javascript:
можно использовать аналогичный паттерн #<a href="javascript:linkClicked()">foo</a>
<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'
, который сделает политику немного менее безопасной.Шаг 4. Добавьте резервные политики для поддержки Safari и устаревших браузеров #
'strict-dynamic'
необходимо указать https:
в качестве резервной политики для поддержки Safari — единственного крупного браузера, не поддерживающего 'strict-dynamic'
. При использовании такой конфигурации:'strict-dynamic'
, будут игнорировать https:
, так что безопасность политики не снизится.javascript:
, поскольку при наличии хеша или одноразового номера параметр 'unsafe-inline'
игнорируется.'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-политику #
Content-Security-Policy-Report-Only
(статья о Reporting API). Режим отправки отчетов удобен для безопасного тестирования в производственной среде изменений, которые могут потенциально нарушить работу сайта, таких как новая CSP-политика. В режиме отправки отчетов CSP-политика не влияет на поведение приложения: сайт продолжает работать так же, как и до этого. Однако браузер будет отображать в консоли сообщения и отчеты о нарушении CSP-политики, когда встречает несовместимый с ней код (и таким образом позволит диагностировать проблемы, которые возникли бы у конечных пользователей).Content-Security-Policy
. Только после выполнения этого шага CSP-политика начнет защищать ваше приложение от XSS-атак. Установка CSP-политики при помощи серверного HTTP-заголовка является более безопасной, чем использование тега <meta>
; если у вас есть возможность, используйте заголовок.Ограничения #
javascript:
). Однако в зависимости от типа применяемой CSP-политики (на основе одноразовых номеров, хешей, а также с использованием 'strict-dynamic'
или без него) существует ряд случаев, на которые ее действие не распространяется:src
элемента <script>
, защищенного одноразовым номером.document.createElement('script')
), включая библиотечные функции, создающие DOM-узлы script
на основе передаваемых им аргументов. К таким функциям относятся некоторые распространенные API, такие как .html()
в jQuery, а также .get()
и .post()
в jQuery < 3.0.eval()
, setTimeout()
и ряд других редко используемых API, если политика содержит параметр 'unsafe-eval'
.Материалы для дальнейшего чтения #