Блог

Web Cache Poisoning: один запрос — тысячи заражённых пользователей.

Poisoning_2
Linux

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-Host
X-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-заголовках.

Leave your thought here

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Поддержать автора

Если вам понравилась статья — вы можете поддержать автора.

Crypto donation button by NOWPayments