[Перевод] Висячий указатель в HTTP/2 довёл curl до RCE: разбор CVE-2026-92392 от Стенберга

В релизе curl 8.23.0 мы закрыли одну из худших уязвимостей за последние годы. Мы оценили её как HIGH. Это CVE-2026-92392, «HTTP/2 upload Use-After-Free».
Ниже — что за баг, к чему он может привести, почему так вышло и что мы делаем, чтобы такую ошибку было сложнее внести снова.
Объяснение сильно упрощено. Все подробности и полностью воспроизводимый Docker-образ — в исходном отчёте. Рекомендую его прочитать: и отчёт, и воспроизведение сделаны отлично.
Use-after-free
Это классическая проблема языков без гарантий безопасности памяти, вроде C: остаётся висячий указатель на уже освобождённую память, и программа всё равно им пользуется.
Особенно плохо, когда злоумышленник может повлиять на то, что окажется в этой памяти. Тогда программу можно заставить сделать совсем не то, что задумывалось.
HTTP/2 мультиплексирован
HTTP/2 передаёт несколько логических передач по одному физическому соединению. curl должен одновременно следить за многими передачами и аккуратно разделять входящие и исходящие данные.
Хэндлы
Для каждой передачи через libcurl приложение создаёт отдельный handle — мы называем его easy handle. Он живёт, пока идёт передача, а когда операция завершена, приложение может его закрыть и освободить. Несколько передач — несколько хэндлов.
Есть и multi handle: «суперхэндл», который содержит любое число easy-хэндлов и позволяет вести много передач одновременно. В любой передаче через libcurl участвует хотя бы один multi handle и один easy handle (иногда multi handle создаётся неявно, «под капотом»).
Условия
Чтобы воспроизвести проблему, приложению (например, консольной утилите curl) нужно выгружать данные на сервер по HTTP/2. libcurl по умолчанию договаривается об HTTP/2, если URL начинается с https:// и сервер согласен.
Когда выгрузка началась, сервер приостанавливает отправку, обнуляя размер окна HTTP/2, отдаёт кусок ответа и сбрасывает поток. Передача завершается, но соединение остаётся. curl при этом корректно «стягивает» передачу целиком, но случайно оставляет висячий указатель на соответствующий easy handle.
Приложение узнаёт, что передача закончилась (с ошибкой), освобождает easy handle и затем закрывает multi handle. Здесь и кроется проблема.
Ошибка
К этому моменту easy handle уже освобождён, но ссылка на него осталась. При закрытии multi handle libcurl неверно понимает ситуацию: выделяет память под приём данных, находит висячий указатель и скачивает данные с сервера — прямо в ту область памяти, где раньше лежал easy handle.
Попутно libcurl отправляет запрос, в котором утекают внутренние детали клиента, в том числе локальные адреса. Это критично: современные ОС используют ASLR, и серверу нужна такая подсказка, чтобы собрать удачную нагрузку для атаки.
Затем libcurl обрабатывает полученные данные в буфере загрузки через хэндл, на который указывает висячий указатель. Он вызывает указатель на функцию, который теперь перезаписан только что скачанными с сервера данными, и тем самым выполняет удалённый код.
Схема
Упрощённая схема из шести шагов:

Что нужно для успешной атаки
Чтобы действительно выполнить удалённый код, должно совпасть множество условий. Мы считаем, что в реальных условиях добиться этого крайне трудно. Из-за этого уязвимость получила «всего лишь» уровень HIGH.
Жертва должна сама пытаться выгрузить данные на вредоносный сервер, то есть сертификат сервера должен оставаться действительным.
Размер выгрузки должен быть примерно подходящим: около 135 КБ.
Атакующему нужно знать архитектуру curl, чтобы собрать и доставить подходящий код.
Нужна определённая последовательность и размеры выделений памяти, чтобы данные сервера попали в нужное место. Этот узор отличается в разных дистрибутивах Linux, версиях и вариантах libc, наборах корневых сертификатов TLS, приложениях на libcurl и так далее.
Играют роль поведение nghttp2 и OpenSSL/TLS 1.3, включая эффекты выделения памяти вокруг session tickets.
Область для полезной нагрузки мала — около 23 байт.
Самый вероятный исход попытки атаки — просто падение. Шанс успешной атаки мы оцениваем как небольшой.
Как нашли
Проблему нашла и сообщила Luna Tong из Anthropic с помощью их нового OSS Scanner. Предположительно, в нём участвуют их модели ИИ.
Как чиним
В последние годы мы сокращаем число внутренних указателей на хэндлы, которые обязаны быть корректными, и вместо них храним идентификаторы передач и ищем хэндл по требованию. Это чуть медленнее, но надёжнее при логических ошибках вроде этой.
Мы продолжаем добавлять тесты и проверять код новыми инструментами и анализаторами, чтобы находить похожие ошибки.
У мультиплексных протоколов вроде HTTP/2 такая сложность в природе: много параллельных передач с разным временем жизни. И в языках без безопасной памяти ошибка с висячим указателем действительно может оказаться фатальной.
Гарантии это не даёт: другой use-after-free в коде может уже быть или появиться позже.
Опять про язык
libcurl написан на C — языке без безопасной памяти. На безопасном языке этой ошибки, вероятно, удалось бы избежать.
Мы развиваем curl почти тридцать лет и за это время получили довольно отполированный движок передачи данных. Долгое время безопасной альтернативы, на которой libcurl остался бы низкоуровневой библиотекой для сотен операционных систем и нескольких десятков архитектур процессоров, просто не существовало. Да и сейчас таких альтернатив почти нет.
Мы не переписываем curl. Ни на другой язык, но и не заново на том же C. Мы будем снова и снова итерировать, шлифовать, улучшать и рефакторить — до скончания времён.
Портирование curl на другой язык (например, на Rust) стало бы чудовищным предприятием: медленным и с кучей новых проблем в краткосрочной и среднесрочной перспективе. К тому же в команде curl нет специалистов по Rust, а libcurl сейчас переносим на гораздо большее число платформ и окружений, чем сам Rust.
График релиза
Эту уязвимость подтвердила команда безопасности curl через считаные часы после того, как её прислали 15 сентября. Мы сразу поняли её значимость и решили ускорить очередной релиз. Но спешить мы не хотели: важно было соблюсти процедуры и не наделать новых ошибок. В итоге мы просто сократили цикл на две недели, с восьми до шести недель после прошлого релиза. Заодно в этот релиз попали исправления многих других уязвимостей и обычных багов.
CVSS
Для опубликованных уязвимостей мы никогда не выставляем оценку CVSS: мы не считаем, что можем сделать это качественно. Но её выставят другие, в том числе и для наших ошибок.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.