Назад к блогу

Cross-tenant утечка в Cloudflare Containers: как тонкое выделение дисков превратилось в канал чтения чужих данных

Cross-tenant утечка в Cloudflare Containers: как тонкое выделение дисков превратилось в канал чтения чужих данных

В статье разбирается природа cross-tenant утечки в Cloudflare Containers, возникшей на стыке тонкого выделения дискового пространства и отключённого обнуления освобождаемых блоков. Автор показывает, как экономия физических блоков превращает их повторное использование в канал чтения остаточных данных соседнего клиента, и прослеживает цепочку от конфигурации пула до спецификации OCI runtime. Материал будет полезен тем, кто занимается изоляцией мультитенантной инфраструктуры и хочет понять, почему отдельные штатные механизмы по сложению дают неожиданный побочный эффект.

Разбираем механику уязвимости, из-за которой контейнер одного клиента мог прочитать остаточные байты на диске, ранее принадлежавшем контейнеру другого клиента. Нетривиальность здесь не в одном баге, а в сочетании штатных механизмов: тонкое выделение экономит место, но именно она делает переиспользование физических блоков нормой, а не исключением. Достаточно одной отключённой опции обнуления — и экономия превращается в утечку.

Как устроено выделение хранилища

Каждый контейнер Cloudflare получает записываемый корневой диск через Linux device mapper thin provisioning (dm-thin). Тонкое выделение отдаёт физический блок только тогда, когда виртуальный диск пишет в ранее несопоставленную область: пока область не тронута, физического носителя под ней нет. Затронутые пулы использовали размер тонкого блока 64 КиБ.

Ключевая деталь — что происходит при чтении ещё не сопоставленной области. dm-thin возвращает нули, не выделяя под это физический блок. То есть «пустая» область тонкого тома выглядит нулевой и не тянет за собой никаких данных с носителя.

Размещение контейнера определяет сама Cloudflare: контейнеры автоматически назначаются на подходящие серверы мультитенантной инфраструктуры, и клиент не может выбрать нижележащий хост. Это важно для дальнейшего — атакующий не управляет тем, на какой машине и с какими соседями окажется его нагрузка.

Что происходит с блоками при удалении контейнера

Когда тонкий том, лежащий в основе корневого диска контейнера, удаляется, его физические блоки не обнуляются, а возвращаются в пул, обслуживающий рабочие нагрузки нескольких клиентских аккаунтов. Это и есть точка, где данные одного тенанта оказываются в общем распоряжении.

Дальше всё решает конфигурация пула. В затронутых пулах была включена опция skip_block_zeroing — параметр пула dm-thin, который отключает очистку вновь выделяемых блоков перед их предоставлением: при обычном поведении dm-thin затирает блок, прежде чем отдать его контейнеру, а с этой опцией блок отдаётся как есть. Поэтому при повторном назначении ранее использованного 64-кибибайтового блока полная запись заменяла его прежнее содержимое, а меньшая запись меняла только записанную часть — остаток сохранял данные предыдущего владельца.

Разница между новым и переиспользованным диском на уровне блочного устройства ровно в этом. Несопоставленная область нового тонкого диска отдаётся как нули без физического размещения. Переиспользованный выделенный блок при выключенном обнулении сохраняет прежнее содержимое за пределами перезаписанной части. Запись 4 КиБ заменяла только эту часть 64-кибибайтового блока, а оставшиеся 60 КиБ могли сохранять данные предыдущего контейнера, и последующее прямое чтение с устройства показывало байты, которых новый контейнер никогда не записывал.

Как работает OCI runtime spec с rootfs и mounts

Спецификация OCI runtime задаёт форму конфигурации, но не модель угроз. Корневая файловая система контейнера описывается объектом root: поле path указывает путь к rootfs, а readonly при значении true делает её доступной только для чтения внутри контейнера. Дополнительные точки монтирования перечисляются в массиве mounts, и рантайм обязан монтировать их в указанном порядке. У каждой записи destination задаёт путь внутри контейнера, source — источник (устройство, файл или каталог для bind-монтирования), options — массив строк с опциями файловой системы. Монтирование считается bind-монтированием, если в options присутствует bind или rbind; на Windows рантаймы обязаны поддерживать опцию ro.

Изоляцию между тенантами spec подкрепляет косвенно: readonly=true запрещает запись в корень, опции вроде nosuid, nodev, noexec ограничивают возможности смонтированных файловых систем. Поля uidMappings и gidMappings сопоставляют идентификаторы пользователя и группы исходной файловой системы с идентификаторами в точке монтирования: благодаря этому владельцем данных внутри контейнера оказывается не тот же пользователь, что на хосте, и один контейнер не получает доступ к файлам другого под своим UID/GID. Но spec не устанавливает, как хранилище выделяется и очищается между контейнерами. Гарантии касаются формы и применения конфигурации, а защита от остаточных данных на общем физическом носителе остаётся ответственностью платформы, которая выбирает и настраивает storage-подсистему.

Механика уязвимости по шагам

Утечка требовала совпадения трёх условий:

  1. Физический блок, ранее использованный другим контейнером, возвращался в общий пул и переиспользовался.
  2. В конфигурации пула был включён skip_block_zeroing, поэтому dm-thin не обнулял вновь выделяемый блок.
  3. Новая запись была меньше 64 КиБ, поэтому заменяла лишь часть блока, а остаток сохранял данные прежнего владельца.

Первые два условия контролировала платформа: именно Cloudflare выбирала хост и настраивала пул, а клиент не мог выбрать сервер. Третье зависело от атакующего — он сам писал 4 КиБ в 64-кибибайтовый регион, чтобы прочитать незаписанные 60 КиБ.

Доказательство концепции выполняло шаги: создать контейнер с Workers Paid, открыть корневой диск /dev/vdc, прочитать диск и записать базовый уровень, записать один 4 КиБ блок в каждую выбранную 64 КиБ область, соответствующую свободному пространству ext4, снова прочитать блоки и изучить только не перезаписанные новой частью. Окно утечки возникало на этапе жизненного цикла, когда тонкий том, лежащий в основе корневого диска контейнера, удалялся и его физические блоки возвращались в пул; при последующем переназначении ранее использованного блока новый контейнер мог прочитать остаточные байты.

После устранения уязвимости Cloudflare удалила skip_block_zeroing и дополнительно вывела из эксплуатации старые диски и кэш образов.

Как валидировали уязвимость

Исследователи использовали скрипты, которые выводили только агрегированные счётчики и проверки формата, а не содержимое файлов. В материалы, переданные Cloudflare, не вошли восстановленные значения содержимого или идентификаторы третьих сторон.

Метод сначала проверили на блоках, которые исследователи намеренно создали и удалили в контролируемой тестовой файловой системе, — он правильно отнёс все 162 блока к этой файловой системе.

На шести производственных размещениях скрипты сообщили обо всех 5614 проверяемых директорных блоках, ноль из которых принадлежал файловой системе исследователей, и о 2700 различных чужих директорных inode. Такой подход этичен: восстановленные данные оставались конфиденциальными и были безопасно удалены после отправки, в соответствии с политикой раскрытия HackerOne от Cloudflare.

Масштаб воздействия

Остаточный материал наблюдался на 18 из 24 размещений и на 20 из 22 базовых узлов на четырёх континентах. Это означает, что проблема проявлялась в большинстве протестированных размещений и почти на всех физических машинах, — то есть носила системный, а не единичный характер.

Уязвимость позволяла клиенту с платным аккаунтом Workers восстанавливать остаточные данные из блоков хранилища, ранее использованных другими клиентами Containers на том же хосте. Успешная эксплуатация пересекала границу изоляции арендаторов и могла раскрыть метаданные файловой системы, структуры каталогов, страницы баз данных и данные приложений. В остаточных блоках могли оказаться структуры каталогов, страницы баз данных и структурно полные базы SQLite; в отчёте исследователей также перечислены листинги каталогов, базы SQLite, профили браузера Chromium, файлы .env и файлы с учётными данными.

При этом атакующий не мог выбрать конкретную жертву или получить доступ к активно подключённому диску. Экспозиция зависела от размещения рабочей нагрузки Cloudflare и того, какие ранее освобождённые блоки переназначил dm-thin. Исследователи не продемонстрировали изменение активных данных другого клиента или влияние на доступность рабочих нагрузок.

Что видел клиент во время утечки

Исследователи обнаружили остаточные данные на 18 из 24 размещений и 20 из 22 узлов, причём их скрипты выводили лишь агрегированные счётчики и проверки формата. Изменение активных данных другого клиента или влияние на доступность рабочих нагрузок продемонстрировано не было.

Обнаружение эксплуатации выполнялось на стороне Cloudflare по сохранённой телеметрии дискового ввода-вывода, а не по клиентским логам.

Реакция Cloudflare и устранение

Исправление было применено по всему флоту Containers без изменений конфигурации на стороне клиента. Первым шагом стало удаление skip_block_zeroing из конфигурации пула dm-thin, что вернуло стандартное поведение — очистку новых выделяемых блоков перед их предоставлением контейнеру.

Однако обнуление новых выделений не очищало блоки, уже отображённые в существующие тонкие устройства, включая кэшированные снапшоты OCI-слоёв. Кэшированные снапшоты OCI-слоёв — это заранее подготовленные на каждом хосте тонкие устройства для слоёв образа контейнера, которые переиспользуются при запуске новых контейнеров; обычное обнуление при новом выделении их не затрагивает, потому что их блоки уже отображены. Эти отображения существовали и в работающих дисках контейнеров, и в кэше подготовленных снапшотов для слоёв OCI-образов на каждом хосте. Поэтому дополнительно были выведены из эксплуатации все работающие диски контейнеров и удалены кэшированные снапшоты образов, созданные до митигации: хосты осушались в непиковые часы, виртуальные машины перезапускались, кэш образов очищался, чтобы диски и кэшированные слои были пересозданы с нулированными аллокациями.

Таймлайн:

  • 7 сентября, 06:13 UTC — Cloudflare завершила раскатку изменений и начала очистку старых данных пула.
  • 14 сентября, 10:50 UTC — исследователи сообщили, что их proof of concept перестал работать.
  • 14 сентября, 12:52 UTC — Cloudflare выплатила исследователю вознаграждение.
  • 19 сентября, 15:03 UTC — Cloudflare завершила очистку всех кэшированных снимков, существовавших до митигации, на затронутом флоте.

Проверка, не эксплуатировал ли уязвимость кто-то ещё

Cloudflare изучила сохранённую историческую телеметрию дискового ввода-вывода контейнерной инфраструктуры, используя proof of concept исследователей и собственное внутреннее воспроизведение как эталонную активность. Опорой послужила характерная связь записей и чтений: запись 4 КиБ в ранее не отображённую область могла вызвать выделение переиспользуемого блока хранения 64 КиБ, а при отключённом обнулении оставшиеся 60 КиБ могли сохранять данные предыдущего контейнера, так что последующие чтения могли восстановить существенно больше данных, чем перезаписал новый контейнер.

По этим характеристикам Cloudflare разработала сигнатуры обнаружения и применила их к доступной исторической телеметрии. В результате была выявлена активность, относящаяся к исследователям и инженерам Cloudflare, проводившим авторизованную проверку, и не выявлено дополнительной активности, согласующейся с сообщённой техникой. Вывод: не было доказательств, что этот конкретный вектор атаки эксплуатировался кем-либо ещё, и не было найдено доказательств злонамеренного использования уязвимости.

Пограничные случаи после фикса

Первичная митигация не закрывала все сценарии. Обнуление новых выделений не очищало блоки, уже отображённые в существующие тонкие устройства. Новый контейнер мог унаследовать отображения из кэшированного слоя без повторного выделения блоков, что позволяло читать остаточные байты в неиспользуемых регионах, включая свободное пространство ext4, через сырые чтения /dev/vdc. Именно поэтому потребовался второй этап — вывод из эксплуатации работающих дисков и удаление кэшированных снапшотов, чтобы диски и кэшированные слои были пересозданы с нулированными аллокациями.

Что из этого следует на практике

Изоляция тенантов на уровне блочного устройства не сводится к тому, что «у каждого свой диск». Тонкое выделение по своей природе переиспользует физические блоки, и граница между арендаторами проходит ровно там, где платформа решает, обнулять ли блок перед повторной выдачей. Отключение обнуления — это не мелкая настройка, а снятие последнего барьера между данными разных клиентов.

Обнуление при выделении и очистка уже существующих отображений — разные задачи. Первое закрывает новые аллокации, второе — блоки, уже вшитые в работающие тонкие устройства и в кэшированные снапшоты слоёв образов. Митигация, затрагивающая только первую, оставляет читаемыми остаточные байты в унаследованных отображениях.

Спецификация OCI runtime описывает форму конфигурации rootfs и mounts, но не даёт гарантий по изоляции данных на общем физическом носителе. Ответственность за то, как хранилище выделяется и очищается между контейнерами, лежит на платформе, и именно там эта уязвимость и возникла.

Наконец, экспозиция зависит от размещения: клиент не выбирает хост, поэтому не может нацелиться на конкретную жертву или на активно подключённый диск. Но это ограничивает лишь прицельность, а не сам факт утечки — остаточные данные оказывались доступны на большинстве проверенных размещений и почти на всех узлах.

Источники

Похожее