За счёт трансляции HTTP/2 в HTTP/1.1 при проксировании запроса через Cloudflare можно достичь увеличения объёма полезной нагрузки в 783 раза. Это коэффициент payload-to-payload, без учёта накладных расходов на сетевом, транспортном уровне и уровне представления, а также без учёта расхода трафика на инициализацию HTTP/2 соединения (отправка client preface, settings, ack и первого прогревочного запроса).
Как это работает?
Cloudflare по умолчанию использует HTTP/1.1 при подключении к origin серверу. Использование TLS также опционально, зависит от выбранного типа соединения с origin сервером. В режиме full — используется, в режиме flexible — нет.
При этом компания предоставляет поддержку HTTP/2 и HTTP/3 на пути client-edge, что позволяет уменьшить затраты пользователя на соединение, ускорить загрузку ресурсов и использовать функции более новых версий протокола HTTP.
Это создает проблему, когда большая часть клиентов включает поддержку современных протоколов для пользователей, но забывает или осознанно не решается использовать HTTP/2 на участке edge-origin, что вызывает асимметричное использование ресурсов для пользователя и origin сервера.
В чем проблема?
Заключается проблема в том, что HTTP/2 использует алгоритм HPACK для сжатия заголовков и экономии трафика. Переданные заголовки вносятся в динамическую таблицу для обеих сторон, и далее в процессе обмена запросами могут использоваться короткие индексы. Конечно, при легитимном использовании это не вызывает проблем, так как разница незначительна, чувствительные заголовки (например, некоторые cookie) не индексируются, и воспринимается это как приятная оптимизация, немного уменьшающая расход трафика.
Когда это становится существенным?
Вредить это начинает в случае намеренного злоупотребления. HTTP/2 позволяет ставить лимит на размер динамической таблицы, но это, по большей части, защита от уязвимости HPACK Bomb, которая заполняет оперативную память сервера. Поэтому лимиты устанавливаются достаточно щедрые, ведь это позволяет эффективнее экономить трафик.
Так у нас появляется возможность добавить достаточно большой заголовок в динамическую таблицу, и отправлять сжатые запросы с индексом этого заголовка. Но даже при таком подходе коэффициент увеличения размера запроса при трансляции сжатого HTTP/2 в HTTP/1.1 хоть и является существенным, но всё-таки это ожидаемое и очевидное следствие декомпрессии HPACK и последующей трансляции в HTTP/1.1.
Как ещё сильнее увеличить коэффициент?
Чтобы добиться коэффициента усиления в 783, необходимо ещё сильнее увеличить конечный размер запроса, при условии, что мы уже можем упираться в лимиты размера динамической таблицы, которые у Cloudflare составляют 8192 байта. Чтобы достичь этого, мы можем просто отправить несколько одинаковых, очень длинных заголовков в одном запросе, которые будут сжаты HPACKом и превратятся в короткие индексы.
Несмотря на то, что отправка повторяющихся идентичных заголовков не имеет известного мне легитимного применения, Cloudflare следует спецификации и передаёт все заголовки дальше, даже не объединяя их в один (за исключением cookie).
Конкретный кейс
Таким образом мне удалось отправить HTTP/2 запрос с девятью заголовками, который в сжатом виде занимает 18 байт (9 байтов frame header + 9 байтов payload), а после трансляции в HTTP/1.1 — 14094 байта. Запрос включает 4 pseudo заголовка, являющихся обязательными, пустой user-agent, так как Cloudflare не пропускает запросы без него, и 4 идентичных раздутых заголовка. Где будет основная часть мусора — не важно, однако можно использовать Cookie заголовки без флага never index. В таком случае, эти заголовки при трансляции будут объединены в один очень длинный Cookie заголовок, что немного снизит коэффициент, однако позволит, например, дополнительно нагрузить парсер атакуемого веб-сервера.
Также в тестах использовались заголовки, полностью состоящие из маленьких латинских «a», потому что при первой отправке заголовка HPACK позволяет сжать его алгоритмом Хаффмана по таблице, определенной в спецификации HPACK (RFC 7541 Appendix B). Символ «a» кодируется 5 битами, что позволяет снизить затраты на отправку первого запроса.
Дополнительно, в первом запросе сразу могут идти 4 заголовка, потому что HPACK кодирует каждый заголовок независимо, а значит, даже если мы использовали заголовок впервые, в запросе будет один заголовок, переданный в виде текста либо сжатый алгоритмом Хаффмана, и 3 индекса, ссылающиеся на него. Поэтому в первом запросе коэффициент усиления будет довольно низким, а в последующих — 780+.
Подводные камни
Стоит упомянуть, что коэффициент зависит от множества факторов. Например, в зависимости от длины доменного имени может меняться требуемая длина заголовков. А иногда лучшим вариантом будет использовать заголовки меньшей длины, но дописать несколько символов «a» в pseudo-заголовок «:path», если это возможно. При этом не стоит забывать, что Cloudflare передаёт и информацию о соединении, например, реальный IP адрес клиента. Поэтому коэффициент для клиента с IP 0.0.0.0 и 255.255.255.255 будет отличаться.
Дополнительно я заметил, что Cloudflare иногда отказывается проксировать сжатые запросы, проксируя аналогичные несжатые запросы. Вероятно, у них есть внутренний механизм, который каким-то образом ограничивает потенциал подобных атак. Если соотношение количества символов в заголовках, отправленных в виде индекса, при делении на количество индексов в запросе, превышает ~1536, Cloudflare отправляет RST_STREAM, не обрывая всё соединение. Этот коэффициент был вычислен эмпирическим путем.
Еще ограничения и способы их обойти
Подсчитанный мной коэффициент учитывает размер расшифрованной полезной нагрузки, payload’а, но игнорирует накладные расходы сетевого, транспортного и других уровней модели OSI. Мне кажется важным обратить внимание на то, что в случае, если мы будем высчитывать коэффициент на сетевом уровне (L3), он будет значительно ниже, в районе 100, в зависимости от различных факторов. Но несмотря на это, мы можем приблизить коэффициент усиления на сетевом уровне к тому, который мы получали, высчитывая размеры пэйлоадов. Для этого нам необходимо использовать мультиплексирование, которое и является основной фичей HTTP/2. Мы можем отправить около 78 параллельных HTTP/2 запросов внутри одного TCP пакета, и тогда соотношение трафика, отправленного клиентом, и трафика, полученного сервером, вновь будет близиться к 700. Конкретные коэффициенты Вы можете вычислить самостоятельно, так как они зависят от того, какие пакеты мы учитываем, в какой сети отправляем данные и даже от того, какую версию TLS используем.
Дополнительные проблемы
Сам принцип работы довольно прост, и последствия могут быть усложнены различными подходами к атаке. Например, как ранее описанная возможность использовать cookie заголовки для дополнительной нагрузки на процессор атакуемого сервера. В эту же категорию неочевидных проблем можно отнести проблему, вызванную отсутствием мультиплексирования в HTTP/1.1.
Когда мы отправляем множество параллельных HTTP/2 запросов на сайт, Cloudflare, транслируя их в HTTP/1.1, открывает множество параллельных TCP соединений к origin серверу, чтобы передать эти запросы также параллельно. Таким образом, тратится как процессорное время на TCP Handshake, так и, в случае использования режима Full, время и память на TLS шифрование в этих соединениях. После быстрого скачка, эти соединения могут висеть до 900 секунд, пока Cloudflare или Origin веб-сервер не закроют их по таймауту, что тоже дополнительно расходует ресурсы сервера.
Проблема дубликатов
Казалось бы, дубликаты заголовков зачастую не имеют легитимного применения, или, как минимум, мне такие случаи не известны. Возникает вопрос, почему же Cloudflare не удаляет дублирующиеся заголовки?
Проблема в соответствии спецификации, которая хоть и написана не очень понятно, но по сути передает довольно четкие границы, какие заголовки дублировать нельзя, а именно: заголовки, где значение предполагается не как список, а как одно значение. То есть, на самом деле, дублировать можно любые кастомные заголовки при условии, что вы уверены в том, что сервер знает как их парсить в случае, если их значения будут представлены как единая строка, разделенная запятыми.
То есть, смысловая нагрузка заголовков вообще не фигурирует в спецификации. Допускается сценарий, в котором для корректной работы приложения необходимо передавать список с несколькими идентичными значениями, хоть такой пример придумать и трудно.
Как защититься от подобных атак?
Наверное, единственный и самый очевидный способ защиты от подобной атаки — использование HTTP/2 to origin. Эту настройку можно найти в настройках Cloudflare, раздел Speed → Settings → Protocol Optimization. Для корректной работы так же требуется режим Full или Full (strict), и явная поддержка HTTP/2 origin’ом, то есть наличие h2 в ALPN.
ссылка на оригинал статьи https://habr.com/ru/articles/1063428/