Ваш скрипт аналитики — это дыра в вашей Content-Security-Policy
Строгая CSP закрывает XSS. Сторонний тег аналитики снова его открывает. Почему host-allowlist и отсутствие SRI подрывают политику — и что чинит first-party трекер.
Content-Security-Policy — это единственная самая эффективная защита от межсайтового скриптинга. В тот момент, когда вы добавляете сторонний тег аналитики, вам обычно приходится проделать в ней дыру. Именно в этой дыре и живёт большинство реальных обходов CSP.
Причина структурная, а не случайная. Скрипты аналитики, ориентированные на слежку, спроектированы так, чтобы подгружать дополнительный код в рантайме — с доменов, которые вы не контролируете. Строгая CSP существует именно для того, чтобы это запретить.
Host-allowlist — это слабый способ доверять скрипту
Старый способ разрешить вендора аналитики — это host-allowlist:
Content-Security-Policy: script-src 'self' https://www.google-analytics.com https://www.googletagmanager.com;
OWASP больше не рекомендует этот паттерн. Allowlist-ы легко обходятся: любой разрешённый origin, на котором есть JSONP-эндпоинт, открытый редирект или загруженные пользователями скрипты, превращается в вектор XSS. Тег-менеджеры делают ситуацию хуже, потому что они существуют именно для того, чтобы внедрять дополнительные скрипты с произвольных origin-ов.
Современная ведущая практика — это строгая CSP, построенная на per-response nonce плюс strict-dynamic:
Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none';
С strict-dynamic браузер доверяет скриптам, загруженным уже доверенным скриптом — а это ровно то поведение, на которое опирается тег-менеджер. Это удобно для вендора и опасно для вас: один скомпрометированный тег теперь получает неявное разрешение загружать что угодно.
SRI не работает на тех скриптах, которым он нужен больше всего
Subresource Integrity позволяет браузеру проверить загруженный файл по криптографическому хешу перед его выполнением:
<script
src="https://cdn.example.com/tracker.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"></script>
SRI поддерживает sha256, sha384 и sha512, и браузер отклоняет ресурс при любом несовпадении. Это чистая защита от скомпрометированного CDN — для статичных файлов с зафиксированной версией.
Для аналитики он рушится. Теги вендоров намеренно изменяемы: файл по тому же URL меняется всякий раз, когда вендор выкатывает новые функции, поэтому зафиксированный хеш сломал бы сбор данных при следующем деплое. На практике команды убирают SRI именно с тех скриптов, у которых самый широкий охват страницы. Пересмотр рисков цепочки поставок в октябре 2025 года оценил небезопасную реализацию SRI как высокую степень серьёзности именно по этой причине.
Trusted Types поднимают планку в феврале 2026
У DOM-XSS-половины проблемы теперь есть ответ на уровне браузера. Начиная с Baseline February 2026, Trusted Types доступны во всех актуальных браузерах. Принудительное применение включается так:
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types default;
После этого DOM-стоки вроде Element.innerHTML отклоняют сырые строки и принимают только значения, выпущенные зарегистрированной политикой:
const policy = trustedTypes.createPolicy("default", {
createHTML: (input) => DOMPurify.sanitize(input),
});
el.innerHTML = policy.createHTML(userInput); // ok
el.innerHTML = userInput; // throws TypeError
Это действительно мощно. Это также именно тот вид правил, который легаси-аналитика и код тег-менеджеров регулярно нарушают, потому что эти скрипты пишут в innerHTML и внедряют узлы <script> как обычные строки. Включение Trusted Types часто означает, что сначала нужно отключить вашего вендора аналитики.
First-party трекер вписывается в строгую политику, вместо того чтобы бороться с ней
Конфликт исчезает, когда скрипт аналитики небольшой, самодостаточный и обслуживается с вашего собственного origin. Трекеру, который не подгружает дополнительный код, не нужны ни strict-dynamic, ни хост вендора в вашем allowlist:
Content-Security-Policy: script-src 'self'; object-src 'none'; base-uri 'none'; require-trusted-types-for 'script';
Вот и всё. Никакого стороннего origin, никакой эскалации nonce, никакого исключения, вырезанного под тег-менеджер.
Трекер Monoid весит примерно 2 КБ, не имеет зависимостей и никогда не вызывает DOM-XSS-сток. Он не пишет в innerHTML, не внедряет скрипты и не читает и не устанавливает cookie или storage. Он отправляет один keepalive-запрос на просмотр страницы на /collect и в остальное время простаивает:
fetch('/collect', {
method: 'POST',
body: JSON.stringify({ site_id, path, referrer, screen_w }),
keepalive: true,
});
Поскольку файл статичен, к нему можно применить SRI, и он никогда не сломается — зафиксированный хеш остаётся валидным до тех пор, пока вы сами не решите обновить скрипт. А поскольку идентичность — это одностороннний ежедневный хеш, SHA-256(IP | UA | SALT_SECRET | YYYY-MM-DD), нет никакого межсайтового профилирования, которое могла бы вообще утечь ослабленная CSP.
Этот паттерн обобщается и за пределы аналитики: каждый скрипт, который вы убираете из своего allowlist, — это целый класс обходов XSS, который вы убираете вместе с ним. Самый безопасный сторонний скрипт — это тот, который вы не загружаете.
Comments
Loading comments…