В 2019 году выявили уязвимость CPDoS Cache Poisoned Denial of Service) на сети CDN, которая позволяет отравить HTTP кэш CDN провайдера и вызвать отказ в обслуживании. Много хайпа уязвимость пока не собрала, так как не была замечена в реальных атаках. Но об одном из способов отравления кэша хочется поговорить отдельно. HTTP Method Override.
Если другие варианты эксплуатации уязвимости так или иначе опираются на баги или особенности модификации запросов посредником, то в основе варианта Method Override лежит одноименная тактика, которая не является частью стандарта HTTP, несет вместе с собой дополнительные проблемы, и которая возникла и распространилась из-за небрежного отношения к безопасности. Вот ее мы и рассмотрим.
Сама необходимость переопределения метода в запросе возникла из-за того, что некоторые Web Application Firewalls и реализации HTTP-клиентов были очень ограничены и не позволяли выполнять методы, отличные от GET и POST. Проблема даже не в том, что это было ограничение реализации, а в том, что это намеренное ограничение HTTP-клиентов политикой безопасности.
Понятно, что все проводилось из лучших побуждений, чтобы отсечь скрафченный трафик, нестандартный для обычных HTTP-клиентов. Но в погоне за безопасностью отрезали все методы кроме GET и POST. Возможно потому, что это единственные методы, которые не опциональны и обязательны для серверов общего назначения.
Для чего потребовалось вводить настолько строгое ограничение не понятно. Да, атаки с внедрением различных символов с целью сбить с толку парсер — это просто конек текстовых протоколов. Но ведь можно было разрешить чуть больше методов, например взять хотя бы те, что описаны в самом стандарте или зарегистрированы в IANA. Совсем убирать проверку метода тоже не стоило, но можно было бы набрать некоторое количество наиболее популярных методов и исключить из них те, которые меняют протокол взаимодействия и ломают работу с соединениями на прокси сервере (CONNECT). Но нет, получилась политика безопасности, которая ввела излишние ограничения и запреты для клиентов.
И клиентов ограничили совсем не тех. Хотели ограничить вариативность сообщений от HTTP-клиентов, а ограничили клиентов, которых защищали эти WAF, конечные серверы приложений и их разработчиков. Теперь разработчикам остались доступны только два метода, которых не всегда хватало, чтобы описать логику работы HTTP-клиента.
Следовало ожидать, что это излишнее ограничение рано или поздно начнет мешать веб-разработчикам. Ирония в том, что так просто от таких WAF не избавиться. Особенно, когда они стоят у клиентов или провайдеров. Оспаривать чужие политики безопасности — гиблое дело.
Благодаря гибкости HTTP обойти такое ограничение не составляет труда, достаточно добавить что-то в запрос, где можно было бы переопределить метод. Строгий WAF будет проверять метод только в Request Line (первой строке запроса) и будет счастлив, увидев там одобренный GET или POST. А бэкэнд сможет распарсить добавленный элемент и извлечь из него настоящий метод.
Можно нагуглить кучу статей, реально кучу о том, как нехорошие прокси ломали REST приложения, и как авторам приходилось передавать реальный метод в отдельном заголовке. В них во всех предлагают ввести примерно одни и те же заголовок (X-HTTP-Method, X-HTTP-Method-Override или X-Method-Override — написание несколько варьируется) для обозначения переопределенного метода. Очень и очень редко можно найти упоминания, что можно использовать для той же цели query-компонент URI.
Чего в этих статьях нет, так это секции «Security Considerations». А они как раз есть.
Иногда разработчики веб-приложений забывают, что между клиентом и сервером могут находиться промежуточные участники, которые взаимодействуют по HTTP протоколу: прокси, веб-кеши провайдеров, CDN и WAF. Распространение TLS здорово снижает шанс, что между клиентом и сервером окажется промежуточный участник. Скорее всего единственным прокси между клиентом и бэкэндом будет свой собственный сервер с Nginx. И такую конфигурацию достаточно легко протестировать на типовых сценариях перед релизом.
Но мы въезжаем в век CDN, и все больше приложений будут прятаться за CDN, которые читают пользовательский трафик и манипулируют им. Бэкенды напрямую почти никогда не обслуживают пользователей, а прячутся за обратными прокси для повышения отзывчивости и производительности. Поэтому придется помнить о том, как переопределение метода может повлиять на обработку запроса на сервере-посреднике.
Атаки, о которых я хочу поговорить, применимы в первую очередь для HTTP/1.1. HTTP/2 в чем-то наследует поведение старого стандарта, в чем-то идет своим путем, поэтому применимость каждой атаки к новому стандарту будет рассмотрена отдельно.
Чаще всего промежуточные серверы не учитывают переопределение метода, не проверяют заголовки семейства X-HTTP-Method-Override и работают с запросом используя его основной метод из Request Line. И поскольку переопределенный метод не входит в ключ для поиска запроса в кэше (метод + URI), то отличить POST от POST + X-HTTP-Method-Override: DELETE такие серверы не могут. А значит, нельзя разрешать кэширование любых запросов к некоторому URI, если бэкенд может отслеживать и выполнять переопределенные методы.
В документе CPDoS есть хороший пример, что происходит, если закэшировать такой запрос. Когда злоумышленник маскирует POST запрос под GET запрос, прокси не распознает подмены и обращается с запросом, как с легитимным GET-запросом. Бэкенд же распознает переопределенный метод и выполняет тот глагол, что описан в заголовке X-HTTP-Method-Override — POST. Так как для целевого URI метод POST не определен, сервер генерирует ошибку. Дальше ответ бэкэнда сохраняется в кэш, как ответ на изначальный метод — GET. Теперь любой следующий GET-запрос на тот же URI будет возвращать закэшированную ошибку.
На деле атака немного шире, чем представлено в документе. Авторы сконцентрировались на сохранении ошибки в кэш, что не везде (уже) может воспроизводиться. Но если запрошенный метод для выбранного URI определен и будет успешно выполнен, то прокси получит ответ со статусом 200 и закэширует его. Тогда последующие запросы того же URI получать ответы на совсем не тот метод. В таком сценарии уже нет требования с ошибкой кэширования 4ХХ ответов, как в оригинальном описании CPDoS.
Может произойти и обратная проблема. Если добропорядочный HTTP-клиент отправляет запрос GET + X-HTTP-Method-Override: PATCH (это плохо, но об этом позже), а в кэше уже есть ответ на GET, то клиент получит именно этот закэшированый ответ. При этом бэкенд никогда не получит PATCH запрос, что может нарушить логику приложения и на клиенте, и на сервере.
Уменьшить влияние на кэш можно, построив правильные политики кэширования и разделив ресурсы на две группы: те, для которых операция переопределения метода недопустима или не требуется, ответы на них можно кэшировать, и те, для которых операция переопределения метода необходима, любое кэширование таких ответов недопустимо. Но чем меньше ресурсов кэшируется, тем меньше пользы от CDN и тем больше трафика доходит до бэкенда, тем больше приложение подвержено HTTP-флуду.
Поэтому лучше максимально задействовать HTTP-кэш, для этого нужно, чтобы кэш-сервер мог различить запросы с разными переопределенными методами. Первый способ — перенести переопределение метода а компонент query в URI:
Теперь запросы с разными методами выглядят по-разному для кэша, так как ключ у них получается разный. Некоторые прокси предпочитают не кэшировать ответы на запросы, содержащие query компонент в URI. Но это скажется только на эффективности кэширования. Проблемы с некорректным кешированием такой способ решает всегда.
Другой способ — оставить переопределение метода в отдельном заголовке, но ввести вторичный ключ для поиска ответа в кэше. Это возможно при помощи заголовка Vary. Сервер при обслуживании запроса повторит заголовок с переопределением метода и отразит имя этого заголовка в заголовке Vary. Тогда при следующих запросах кэш-сервер будет использовать значение переопределенного метода в качестве вторичного ключа при поиске запроса в кэше.
Этот способ работает, если промежуточный сервер умеет работать с вторичными ключами. Обычно это так, но уровень доверия к прокси, который режет все методы, кроме GET и POST, обычно ниже и это лучше проверить.
Переопределение метода через какую-либо сущность внутри тела запроса обладает ровно теми же недостатками, что и переопределение через дополнительный заголовок — находится вне видимости кэша.
Даже если атаки на кэш закрыты, это еще не все. Злоумышленник переопределением метода может попытаться изменить фрейминг ответа и тем самым нарушить соответствие пар запрос-ответ для других клиентов. Или заставить серверную часть приложения обработать один и тот же запрос несколько раз.
Самое главное, что для этого требуется — промежуточный сервер, работающий в режиме reverse-proxy. То есть любой кэширующий или CDN сервер. Такой прокси поддерживает относительно небольшое количество соединений с бэкэндом, и мультиплицирует в каждом из них запросы от множества клиентов. Это нужно и чтобы забрать нагрузку по поддержке большого числа соединений с клиентами с бэкэндов на прокси-сервер, и для балансировки нагрузки между бэкэндами. Терминирование TLS-соединений так же происходит на прокси, клиентские соединения никогда не подключаются напрямую к бэкэнду.
Поскольку теперь в одном соединении между бэкэндом и прокси будут находиться запросы от разных клиентов, необходимо поддерживать четкое соответствие пар запрос-ответ. Большая часть прокси не пайплайнит (конвейеризирует) запросы до бэкенда и работает с ним в режиме запрос-ответ. Режим запрос-ответ проще и подвержен фактически одной угрозе — блокировка соединения. Если заставить соединение повиснуть на одной паре запрос-ответ, то можно вызвать задержку или даже отказ обработки следующих запросов (например, если добиться переполнения очередей запросов на прокси).
Более производительные прокси пайплайнят запросы до бэкенда — это позволяет передать серверу сразу пачку запросов и ждать их выполнения. Производительность выше, но и угроз больше. Во-первых, проблема head-of-line blocking никуда не исчезает — даже если бэкэнд может разгребать конвейер запросов и выполнять их параллельно, они не могут быть отправлены, если «завис» первый из них. Во-вторых, если сломать фрейминг ответа, то можно запутать прокси и нарушить соответствие пар запрос-ответ, тогда некоторые клиенты могут получить чужие ответы, или хотя бы добиться мгновенного закрытия соединения с бэкэндом.
Самое простое и самое веселое переопределение метода — подменить GET глаголом HEAD. Если ответ на первый имеет тело, то второй — нет. При этом все остальные заголовки у них одинаковые, в том числе те, которые обеспечивают фрейминг запроса. Когда прокси перенаправит такой переопределенный HEAD серверу, он будет ожидать от сервера не только заголовки ответа, но и тело ответа, которое бэкенд не собирается отправлять. Если прокси и сервер взаимодействуют в режиме запрос-ответ, то соединение «повиснет» до момента пока не разорвется по таймауту.
Если же сервер пришлет следующие ответы (конвейерный режим) — то они могут быть распарсены не как самостоятельные ответы, а как часть предыдущего, незаконченного ответа на GET. Прокси поместит их (или их часть) в тело «GET»-ответа и отправит злоумышленнику, чтобы он их почитал. Можно скрафтить такой псевдо-GET на получение объемного файла и дампнуть немного трафика между прокси и бэкендом. Успешность зависит от того, как бэкэнд расставляет заголовки Content-Length и Transfer-Encoding: chunked для фрейминга сообщений. Первый почти всегда позволит получить дамп, второй чаще будет выдавать ошибки парсинга и вызывать разрыв соединения с бэкэндом. Если совсем не повезет, то псевдо-GET может накрыть несколько ответов целиком и закончиться как раз перед очередным ответом. Эту проблему прокси вообще не сможет распознать и для дальнейших ответов в этом соединении соответствие запрос-ответ будет нарушено.
Даже если все, что удалось добиться переопределением метода — закрытие соединения между прокси и бэкендом, то и этого может хватить для атаки. Можно закидывать такими запросами сервис — соединения с бэкэндами будут постоянно рваться. Их не так много, а переоткрытие занимает время, в итоге можно добиться значительного уменьшения производительности связи прокси-бэкенд и тем самым уменьшить пропускную способность сервиса.
Выше я говорил, что запросы вида GET + X-HTTP-Method-Override: PATCH от добропорядочных клиентов это плохо. А плохо это потому, что методов есть два свойства: безопасность и идемпотентность. Безопасность означает, что метод не меняет состояние сервера (read-only), и не интересует нас в контексте этой статьи. Идемпотентность метода гарантирует, что многократное повторное запроса имеет тот же эффект, что и однократное его выполнение. Можно провести такую аналогию:
Как раз это свойство нас и интересует. Если соединение между клиентом и сервером внезапно разорвется, клиент, зная что метод идемпотентен, может автоматически повторить отправку запроса. Так же ведут себя прокси. Неидемпотентые запросы автоматически не повторяются, потому что неизвестно, как они влияют на сервер и что клиент получит в итоге. Думаю, всем знакомы всплывающие окна в браузере: «вы уверены, что хотите повторить запрос?».
Если замаскировать неидемпотентный метод за идемпотентным, то в случае ошибок он не будет отброшен, а будет переотправлен на сервер еще раз. Даже если клиент будет учитывать настоящий метод запроса перед повторной отправкой запроса, это не сильно поможет, потому как прокси-сервер не знает о переопределении метода и будет повторять такие запросы.
Если злоумышленник сможет форсировать разрывы соединений между бэкендом и клиентами, то он сможет вызвать многократное выполнение на сервере неидемпотентных запросов и снизить надежность и предсказуемость приложения. В предыдущем разделе мы как раз нашли способ, как можно вызывать разрывы соединений все тем же переопределением метода. Хотя надо помнить, что интернет — ненадежная сеть по определению и приложение само по себе в опасности.
Чтобы обезопаситься от этой атаки следует в качестве транспорта использовать только те методы, которые не добавляют новых свойств запросу. POST — хорошая кандидатура, поскольку по-умолчанию не характеризуется ни как безопасный, ни как идемпотентный метод.
HTTP/2 изменил способ транспортировки запросов между узлами, но не изменил их лексического смысла. Поэтому в тех атаках, которые касаются значения запроса, HTTP/2 ведет себя так же. А «транспортные» атаки не воспроизводятся, так как уже учтены в стандарте.
Атаки на кэш воспроизводятся аналогичным HTTP/1 образом, защита тоже аналогичная.
Атаки на очередь сообщений не применимы к HTTP/2. HTTP-cообщения в нем разделяются на отдельные фреймы, со отдельными заголовками фрейма, явно определяющими длину и конец сообщения. Как бы атакующий не менял бы метод и не модифицировал бы HTTP-заголовки, на фрейминг сообщения это не повлияет. Украсть ответ уже не получится.
Атаки на повтор неидемпотентных сообщений применима даже с учетом того, что в HTTP/2 есть механизм уведомления о последнем обработанном запросе. В HTTP/2 несколько запросов мультиплицируются в одном TCP и создают таким образом потоки. У каждого потока есть свой номер. Если HTTP/2 сервер разрывает соединение, то он может указать номер последнего обработанного запроса в GOAWAY. Запросы с большим номером переоправлять безопасно всегда, запросы с меньшим — только если они являются идемпотентными. Если запрос с переопределенным методом выглядит для прокси сервера идемпотентным, то прокси будет его переотправлять серверу.
Короткий ответ — никак. Лучше вообще не использовать переопределение метода. И полностью отключить поддержку в бэкенде, если такая имеется. Блокировать HTTP-клиентов, переопределяющих методы. Отказаться от прокси/WAF, который режет «лишние» методы.
Если же с переопределением метода приходится как-то жить, то для предотвращения достаточно правок на бэкенде. Во-первых, переопределение метода желательно производить только через query-компонент URI.
Во-вторых, должен быть белый список трансформации методов: какие допустимы в качестве «транспортного», а какие — результирующего. Не должно быть обобщенных функций трансформации, когда любой метод можно переопределить любым. «Транспортный» метод не должен обладать свойствами безопасности и идемпотентности, если ими не обладает результирующий. Должны быть запрещены опасные трансформации, та же замена GET -> HEAD.
Если прокси реализует только методы GET и POST, а остальные по той или иной причине блокирует — определенно да. Можно оптимизировать в первую очередь для GET и POST, но блокировать другие методы — плохая идея. Которая еще создает пропасть недоверия к продукту: если блокируются базовые вещи, что ожидать от реализации более сложных проблем?
Если вы беспокоитесь о безопасности защищаемых веб-приложений, то возможно стоит подстраховывать приложения от небезопасных политик переопределения метода. Разумеется, в общем случае, не зная деталей реализации веб-приложения, полностью защитить приложение от некорректных переопределений нельзя, но можно частично прикрыть пользователей, которые просто не знают о проблеме. Стоит не только защититься от отравления собственного кэша, но и сделать возможность разрешать или запрещать переопределение для каждого защищаемого приложения. Для этого нужно отслеживать часто используемые заголовки X-HTTP-Method, X-HTTP-Method-Override и X-Method-Override. Отслеживать переопределение в query-компоненте URI нет большого смысла: кэш такой запрос не отравит, а query может быть очень длинным и иметь совсем произвольный формат.
Разработчики систем безопасности, не ограничивайте разработчиков приложений политиками безопасности. Они все равно найдут, как их обойти, и чем гибче протокол — тем легче это сделать. Очень вероятно, что они не будут пинать вас и ждать, пока вы не сделаете ограничения более разумными, а просто обойдут их.
Если вы придумали, как что-то реализовать в протоколе, но это переопределяет или идет в разрез с одним из ключевых концептов стандарта, то наверняка возникнут проблемы совместимости и безопасности. И их нужно освещать одновременно с решением. Каждый раз. Если вы встретили такой совет и не увидели предупреждений безопасности, то не стоит тиражировать совет по всему интернету. Всегда критически подходите к решению и прикидывайте, что может пойти не так.
А с какими проблемами прокси серверов сталкивались вы? Что приходилось обходить и как?
Если другие варианты эксплуатации уязвимости так или иначе опираются на баги или особенности модификации запросов посредником, то в основе варианта Method Override лежит одноименная тактика, которая не является частью стандарта HTTP, несет вместе с собой дополнительные проблемы, и которая возникла и распространилась из-за небрежного отношения к безопасности. Вот ее мы и рассмотрим.
Коротко о CPDoS, если вы его пропустили
Стандарт говорит что, ключом при записи ответа в кэш является связка URI и method из соответствующего запроса.
Поскольку ключ для кэширования получается достаточно узким, а запросы клиентом могут достаточно сильно различаться, то злоумышленник может попробовать создать такой запрос, чтобы для промежуточного кэша он не отличался от других, а обработка бы его на бэкэнд-сервере вызывала бы ошибку. Для этого есть достаточно много возможностей — это как пассивная манипуляция заголовками и телом запроса с целью заставить кэш-сервер и бэкэнд-сервер воспринимать запросы по-разному, так и более активный варинт манипуляции, когда атакующий заставляет кэш-сервер модифицировать запрос при передаче бэкэнду, вызывая тем самым ошибки на бэкэнде. В ряде случаев возникала возможность занести в кэш сообщение от ошибке, которое бы потом возвращалось всем клиентам вместо запрашиваемого ресурса.
Эксплуатация более вероятна если логика работы кэша на промежуточном узле оказывается более агрессивной, чем описано в стандарте. Например, кэшируются ответы, не являющиеся кешируемыми по-умолчанию. Это далеко не всегда происходит из-за ошибок в реализации, иногда стратегии кэширования намеренно делаются более агрессивными, чтобы снизить давление на бэкэнд сервер.
Поскольку ключ для кэширования получается достаточно узким, а запросы клиентом могут достаточно сильно различаться, то злоумышленник может попробовать создать такой запрос, чтобы для промежуточного кэша он не отличался от других, а обработка бы его на бэкэнд-сервере вызывала бы ошибку. Для этого есть достаточно много возможностей — это как пассивная манипуляция заголовками и телом запроса с целью заставить кэш-сервер и бэкэнд-сервер воспринимать запросы по-разному, так и более активный варинт манипуляции, когда атакующий заставляет кэш-сервер модифицировать запрос при передаче бэкэнду, вызывая тем самым ошибки на бэкэнде. В ряде случаев возникала возможность занести в кэш сообщение от ошибке, которое бы потом возвращалось всем клиентам вместо запрашиваемого ресурса.
Эксплуатация более вероятна если логика работы кэша на промежуточном узле оказывается более агрессивной, чем описано в стандарте. Например, кэшируются ответы, не являющиеся кешируемыми по-умолчанию. Это далеко не всегда происходит из-за ошибок в реализации, иногда стратегии кэширования намеренно делаются более агрессивными, чтобы снизить давление на бэкэнд сервер.
Ограничь клиента, меньше может — меньше сломает
Сама необходимость переопределения метода в запросе возникла из-за того, что некоторые Web Application Firewalls и реализации HTTP-клиентов были очень ограничены и не позволяли выполнять методы, отличные от GET и POST. Проблема даже не в том, что это было ограничение реализации, а в том, что это намеренное ограничение HTTP-клиентов политикой безопасности.
Понятно, что все проводилось из лучших побуждений, чтобы отсечь скрафченный трафик, нестандартный для обычных HTTP-клиентов. Но в погоне за безопасностью отрезали все методы кроме GET и POST. Возможно потому, что это единственные методы, которые не опциональны и обязательны для серверов общего назначения.
Для чего потребовалось вводить настолько строгое ограничение не понятно. Да, атаки с внедрением различных символов с целью сбить с толку парсер — это просто конек текстовых протоколов. Но ведь можно было разрешить чуть больше методов, например взять хотя бы те, что описаны в самом стандарте или зарегистрированы в IANA. Совсем убирать проверку метода тоже не стоило, но можно было бы набрать некоторое количество наиболее популярных методов и исключить из них те, которые меняют протокол взаимодействия и ломают работу с соединениями на прокси сервере (CONNECT). Но нет, получилась политика безопасности, которая ввела излишние ограничения и запреты для клиентов.
И клиентов ограничили совсем не тех. Хотели ограничить вариативность сообщений от HTTP-клиентов, а ограничили клиентов, которых защищали эти WAF, конечные серверы приложений и их разработчиков. Теперь разработчикам остались доступны только два метода, которых не всегда хватало, чтобы описать логику работы HTTP-клиента.
Ограничения созданы, чтобы их преодолевать
Следовало ожидать, что это излишнее ограничение рано или поздно начнет мешать веб-разработчикам. Ирония в том, что так просто от таких WAF не избавиться. Особенно, когда они стоят у клиентов или провайдеров. Оспаривать чужие политики безопасности — гиблое дело.
Благодаря гибкости HTTP обойти такое ограничение не составляет труда, достаточно добавить что-то в запрос, где можно было бы переопределить метод. Строгий WAF будет проверять метод только в Request Line (первой строке запроса) и будет счастлив, увидев там одобренный GET или POST. А бэкэнд сможет распарсить добавленный элемент и извлечь из него настоящий метод.
Можно нагуглить кучу статей, реально кучу о том, как нехорошие прокси ломали REST приложения, и как авторам приходилось передавать реальный метод в отдельном заголовке. В них во всех предлагают ввести примерно одни и те же заголовок (X-HTTP-Method, X-HTTP-Method-Override или X-Method-Override — написание несколько варьируется) для обозначения переопределенного метода. Очень и очень редко можно найти упоминания, что можно использовать для той же цели query-компонент URI.
Чего в этих статьях нет, так это секции «Security Considerations». А они как раз есть.
Так ли безопасно переопределение метода?
Иногда разработчики веб-приложений забывают, что между клиентом и сервером могут находиться промежуточные участники, которые взаимодействуют по HTTP протоколу: прокси, веб-кеши провайдеров, CDN и WAF. Распространение TLS здорово снижает шанс, что между клиентом и сервером окажется промежуточный участник. Скорее всего единственным прокси между клиентом и бэкэндом будет свой собственный сервер с Nginx. И такую конфигурацию достаточно легко протестировать на типовых сценариях перед релизом.
Но мы въезжаем в век CDN, и все больше приложений будут прятаться за CDN, которые читают пользовательский трафик и манипулируют им. Бэкенды напрямую почти никогда не обслуживают пользователей, а прячутся за обратными прокси для повышения отзывчивости и производительности. Поэтому придется помнить о том, как переопределение метода может повлиять на обработку запроса на сервере-посреднике.
Атаки, о которых я хочу поговорить, применимы в первую очередь для HTTP/1.1. HTTP/2 в чем-то наследует поведение старого стандарта, в чем-то идет своим путем, поэтому применимость каждой атаки к новому стандарту будет рассмотрена отдельно.
Атаки на кэш
Чаще всего промежуточные серверы не учитывают переопределение метода, не проверяют заголовки семейства X-HTTP-Method-Override и работают с запросом используя его основной метод из Request Line. И поскольку переопределенный метод не входит в ключ для поиска запроса в кэше (метод + URI), то отличить POST от POST + X-HTTP-Method-Override: DELETE такие серверы не могут. А значит, нельзя разрешать кэширование любых запросов к некоторому URI, если бэкенд может отслеживать и выполнять переопределенные методы.
В документе CPDoS есть хороший пример, что происходит, если закэшировать такой запрос. Когда злоумышленник маскирует POST запрос под GET запрос, прокси не распознает подмены и обращается с запросом, как с легитимным GET-запросом. Бэкенд же распознает переопределенный метод и выполняет тот глагол, что описан в заголовке X-HTTP-Method-Override — POST. Так как для целевого URI метод POST не определен, сервер генерирует ошибку. Дальше ответ бэкэнда сохраняется в кэш, как ответ на изначальный метод — GET. Теперь любой следующий GET-запрос на тот же URI будет возвращать закэшированную ошибку.
На деле атака немного шире, чем представлено в документе. Авторы сконцентрировались на сохранении ошибки в кэш, что не везде (уже) может воспроизводиться. Но если запрошенный метод для выбранного URI определен и будет успешно выполнен, то прокси получит ответ со статусом 200 и закэширует его. Тогда последующие запросы того же URI получать ответы на совсем не тот метод. В таком сценарии уже нет требования с ошибкой кэширования 4ХХ ответов, как в оригинальном описании CPDoS.
Может произойти и обратная проблема. Если добропорядочный HTTP-клиент отправляет запрос GET + X-HTTP-Method-Override: PATCH (это плохо, но об этом позже), а в кэше уже есть ответ на GET, то клиент получит именно этот закэшированый ответ. При этом бэкенд никогда не получит PATCH запрос, что может нарушить логику приложения и на клиенте, и на сервере.
Уменьшить влияние на кэш можно, построив правильные политики кэширования и разделив ресурсы на две группы: те, для которых операция переопределения метода недопустима или не требуется, ответы на них можно кэшировать, и те, для которых операция переопределения метода необходима, любое кэширование таких ответов недопустимо. Но чем меньше ресурсов кэшируется, тем меньше пользы от CDN и тем больше трафика доходит до бэкенда, тем больше приложение подвержено HTTP-флуду.
Поэтому лучше максимально задействовать HTTP-кэш, для этого нужно, чтобы кэш-сервер мог различить запросы с разными переопределенными методами. Первый способ — перенести переопределение метода а компонент query в URI:
POST /some-uri HTTP/1.1
X-HTTP-Method-Override: DELETE
↓ ↓ ↓
POST /some-uri?method=DELETE HTTP/1.1
Теперь запросы с разными методами выглядят по-разному для кэша, так как ключ у них получается разный. Некоторые прокси предпочитают не кэшировать ответы на запросы, содержащие query компонент в URI. Но это скажется только на эффективности кэширования. Проблемы с некорректным кешированием такой способ решает всегда.
Другой способ — оставить переопределение метода в отдельном заголовке, но ввести вторичный ключ для поиска ответа в кэше. Это возможно при помощи заголовка Vary. Сервер при обслуживании запроса повторит заголовок с переопределением метода и отразит имя этого заголовка в заголовке Vary. Тогда при следующих запросах кэш-сервер будет использовать значение переопределенного метода в качестве вторичного ключа при поиске запроса в кэше.
Этот способ работает, если промежуточный сервер умеет работать с вторичными ключами. Обычно это так, но уровень доверия к прокси, который режет все методы, кроме GET и POST, обычно ниже и это лучше проверить.
Переопределение метода через какую-либо сущность внутри тела запроса обладает ровно теми же недостатками, что и переопределение через дополнительный заголовок — находится вне видимости кэша.
Атаки на очередь сообщений
Даже если атаки на кэш закрыты, это еще не все. Злоумышленник переопределением метода может попытаться изменить фрейминг ответа и тем самым нарушить соответствие пар запрос-ответ для других клиентов. Или заставить серверную часть приложения обработать один и тот же запрос несколько раз.
Самое главное, что для этого требуется — промежуточный сервер, работающий в режиме reverse-proxy. То есть любой кэширующий или CDN сервер. Такой прокси поддерживает относительно небольшое количество соединений с бэкэндом, и мультиплицирует в каждом из них запросы от множества клиентов. Это нужно и чтобы забрать нагрузку по поддержке большого числа соединений с клиентами с бэкэндов на прокси-сервер, и для балансировки нагрузки между бэкэндами. Терминирование TLS-соединений так же происходит на прокси, клиентские соединения никогда не подключаются напрямую к бэкэнду.
Поскольку теперь в одном соединении между бэкэндом и прокси будут находиться запросы от разных клиентов, необходимо поддерживать четкое соответствие пар запрос-ответ. Большая часть прокси не пайплайнит (конвейеризирует) запросы до бэкенда и работает с ним в режиме запрос-ответ. Режим запрос-ответ проще и подвержен фактически одной угрозе — блокировка соединения. Если заставить соединение повиснуть на одной паре запрос-ответ, то можно вызвать задержку или даже отказ обработки следующих запросов (например, если добиться переполнения очередей запросов на прокси).
Более производительные прокси пайплайнят запросы до бэкенда — это позволяет передать серверу сразу пачку запросов и ждать их выполнения. Производительность выше, но и угроз больше. Во-первых, проблема head-of-line blocking никуда не исчезает — даже если бэкэнд может разгребать конвейер запросов и выполнять их параллельно, они не могут быть отправлены, если «завис» первый из них. Во-вторых, если сломать фрейминг ответа, то можно запутать прокси и нарушить соответствие пар запрос-ответ, тогда некоторые клиенты могут получить чужие ответы, или хотя бы добиться мгновенного закрытия соединения с бэкэндом.
Самое простое и самое веселое переопределение метода — подменить GET глаголом HEAD. Если ответ на первый имеет тело, то второй — нет. При этом все остальные заголовки у них одинаковые, в том числе те, которые обеспечивают фрейминг запроса. Когда прокси перенаправит такой переопределенный HEAD серверу, он будет ожидать от сервера не только заголовки ответа, но и тело ответа, которое бэкенд не собирается отправлять. Если прокси и сервер взаимодействуют в режиме запрос-ответ, то соединение «повиснет» до момента пока не разорвется по таймауту.
Если же сервер пришлет следующие ответы (конвейерный режим) — то они могут быть распарсены не как самостоятельные ответы, а как часть предыдущего, незаконченного ответа на GET. Прокси поместит их (или их часть) в тело «GET»-ответа и отправит злоумышленнику, чтобы он их почитал. Можно скрафтить такой псевдо-GET на получение объемного файла и дампнуть немного трафика между прокси и бэкендом. Успешность зависит от того, как бэкэнд расставляет заголовки Content-Length и Transfer-Encoding: chunked для фрейминга сообщений. Первый почти всегда позволит получить дамп, второй чаще будет выдавать ошибки парсинга и вызывать разрыв соединения с бэкэндом. Если совсем не повезет, то псевдо-GET может накрыть несколько ответов целиком и закончиться как раз перед очередным ответом. Эту проблему прокси вообще не сможет распознать и для дальнейших ответов в этом соединении соответствие запрос-ответ будет нарушено.
Даже если все, что удалось добиться переопределением метода — закрытие соединения между прокси и бэкендом, то и этого может хватить для атаки. Можно закидывать такими запросами сервис — соединения с бэкэндами будут постоянно рваться. Их не так много, а переоткрытие занимает время, в итоге можно добиться значительного уменьшения производительности связи прокси-бэкенд и тем самым уменьшить пропускную способность сервиса.
Автоматический нежелательный повтор сообщений
Выше я говорил, что запросы вида GET + X-HTTP-Method-Override: PATCH от добропорядочных клиентов это плохо. А плохо это потому, что методов есть два свойства: безопасность и идемпотентность. Безопасность означает, что метод не меняет состояние сервера (read-only), и не интересует нас в контексте этой статьи. Идемпотентность метода гарантирует, что многократное повторное запроса имеет тот же эффект, что и однократное его выполнение. Можно провести такую аналогию:
(a = 5)
— идемпотентный запрос, а (a += 2)
— неидемпотентный.Как раз это свойство нас и интересует. Если соединение между клиентом и сервером внезапно разорвется, клиент, зная что метод идемпотентен, может автоматически повторить отправку запроса. Так же ведут себя прокси. Неидемпотентые запросы автоматически не повторяются, потому что неизвестно, как они влияют на сервер и что клиент получит в итоге. Думаю, всем знакомы всплывающие окна в браузере: «вы уверены, что хотите повторить запрос?».
Если замаскировать неидемпотентный метод за идемпотентным, то в случае ошибок он не будет отброшен, а будет переотправлен на сервер еще раз. Даже если клиент будет учитывать настоящий метод запроса перед повторной отправкой запроса, это не сильно поможет, потому как прокси-сервер не знает о переопределении метода и будет повторять такие запросы.
Если злоумышленник сможет форсировать разрывы соединений между бэкендом и клиентами, то он сможет вызвать многократное выполнение на сервере неидемпотентных запросов и снизить надежность и предсказуемость приложения. В предыдущем разделе мы как раз нашли способ, как можно вызывать разрывы соединений все тем же переопределением метода. Хотя надо помнить, что интернет — ненадежная сеть по определению и приложение само по себе в опасности.
Чтобы обезопаситься от этой атаки следует в качестве транспорта использовать только те методы, которые не добавляют новых свойств запросу. POST — хорошая кандидатура, поскольку по-умолчанию не характеризуется ни как безопасный, ни как идемпотентный метод.
То древний HTTP/1.1, как с HTTP/2?
HTTP/2 изменил способ транспортировки запросов между узлами, но не изменил их лексического смысла. Поэтому в тех атаках, которые касаются значения запроса, HTTP/2 ведет себя так же. А «транспортные» атаки не воспроизводятся, так как уже учтены в стандарте.
Атаки на кэш воспроизводятся аналогичным HTTP/1 образом, защита тоже аналогичная.
Атаки на очередь сообщений не применимы к HTTP/2. HTTP-cообщения в нем разделяются на отдельные фреймы, со отдельными заголовками фрейма, явно определяющими длину и конец сообщения. Как бы атакующий не менял бы метод и не модифицировал бы HTTP-заголовки, на фрейминг сообщения это не повлияет. Украсть ответ уже не получится.
Атаки на повтор неидемпотентных сообщений применима даже с учетом того, что в HTTP/2 есть механизм уведомления о последнем обработанном запросе. В HTTP/2 несколько запросов мультиплицируются в одном TCP и создают таким образом потоки. У каждого потока есть свой номер. Если HTTP/2 сервер разрывает соединение, то он может указать номер последнего обработанного запроса в GOAWAY. Запросы с большим номером переоправлять безопасно всегда, запросы с меньшим — только если они являются идемпотентными. Если запрос с переопределенным методом выглядит для прокси сервера идемпотентным, то прокси будет его переотправлять серверу.
Как безопасно переопределить метод
Короткий ответ — никак. Лучше вообще не использовать переопределение метода. И полностью отключить поддержку в бэкенде, если такая имеется. Блокировать HTTP-клиентов, переопределяющих методы. Отказаться от прокси/WAF, который режет «лишние» методы.
Если же с переопределением метода приходится как-то жить, то для предотвращения достаточно правок на бэкенде. Во-первых, переопределение метода желательно производить только через query-компонент URI.
Во-вторых, должен быть белый список трансформации методов: какие допустимы в качестве «транспортного», а какие — результирующего. Не должно быть обобщенных функций трансформации, когда любой метод можно переопределить любым. «Транспортный» метод не должен обладать свойствами безопасности и идемпотентности, если ими не обладает результирующий. Должны быть запрещены опасные трансформации, та же замена GET -> HEAD.
Нужно ли патчить проблемный прокси/WAF?
Если прокси реализует только методы GET и POST, а остальные по той или иной причине блокирует — определенно да. Можно оптимизировать в первую очередь для GET и POST, но блокировать другие методы — плохая идея. Которая еще создает пропасть недоверия к продукту: если блокируются базовые вещи, что ожидать от реализации более сложных проблем?
Если вы беспокоитесь о безопасности защищаемых веб-приложений, то возможно стоит подстраховывать приложения от небезопасных политик переопределения метода. Разумеется, в общем случае, не зная деталей реализации веб-приложения, полностью защитить приложение от некорректных переопределений нельзя, но можно частично прикрыть пользователей, которые просто не знают о проблеме. Стоит не только защититься от отравления собственного кэша, но и сделать возможность разрешать или запрещать переопределение для каждого защищаемого приложения. Для этого нужно отслеживать часто используемые заголовки X-HTTP-Method, X-HTTP-Method-Override и X-Method-Override. Отслеживать переопределение в query-компоненте URI нет большого смысла: кэш такой запрос не отравит, а query может быть очень длинным и иметь совсем произвольный формат.
Чему мы можем научиться в этой истории?
Разработчики систем безопасности, не ограничивайте разработчиков приложений политиками безопасности. Они все равно найдут, как их обойти, и чем гибче протокол — тем легче это сделать. Очень вероятно, что они не будут пинать вас и ждать, пока вы не сделаете ограничения более разумными, а просто обойдут их.
Если вы придумали, как что-то реализовать в протоколе, но это переопределяет или идет в разрез с одним из ключевых концептов стандарта, то наверняка возникнут проблемы совместимости и безопасности. И их нужно освещать одновременно с решением. Каждый раз. Если вы встретили такой совет и не увидели предупреждений безопасности, то не стоит тиражировать совет по всему интернету. Всегда критически подходите к решению и прикидывайте, что может пойти не так.
Вместо послесловия
А с какими проблемами прокси серверов сталкивались вы? Что приходилось обходить и как?