Критическая уязвимость lzma, bz2 и gzip cpython: риск утечки данных и кода

Критическая уязвимость в модулях lzma, bz2 и gzip CPython: риск утечки данных и выполнения кода
------------------------------------------------------------------

В стандартной реализации Python (CPython) обнаружена критическая ошибка в низкоуровневых модулях, отвечающих за распаковку сжатых данных в форматах lzma, bz2 и gzip. Уязвимость получила идентификатор CVE-2026-6100 и оценку 9.1 по шкале CVSS, что относит её к категории критических.

Проблема связана с классами:

- `lzma.LZMADecompressor`
- `bz2.BZ2Decompressor`
- `gzip.GzipFile`

Все они используют реализованные на C внутренние модули `_lzmamodule.c`, `_bz2module.c` и `zlibmodule.c`. Ошибка приводит к обращению к области памяти после её освобождения (use-after-free), что в определённых условиях может позволить атакующему как минимум считывать произвольные данные процесса, а в худшем случае - добиться выполнения собственного кода.

В чём суть уязвимости

Корень проблемы - в обработке ошибки нехватки памяти при распаковке специально подготовленных данных. Сценарий выглядит так:

1. Приложение создаёт объект-декомпрессор, например `lzma.LZMADecompressor()`.
2. В процессе распаковки переданных данных происходит исчерпание доступной памяти, и код на C внутри CPython выбрасывает исключение `MemoryError`.
3. Внутренняя структура объекта частично освобождает память или приходит в некорректное состояние.
4. Приложение, не создавая новый декомпрессор, повторно использует тот же объект для дальнейшей распаковки.
5. При второй попытке распаковки код обращается к уже освобождённой памяти.

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

Почему требуется искусственное исчерпание памяти

Уязвимость активируется только при возникновении `MemoryError`. То есть злоумышленнику нужно добиться ситуации, когда:

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

Сам по себе `MemoryError` не является чем-то необычным для интерпретатора, но целенаправленно воспроизвести его в тестовой среде непросто. Именно поэтому разработчики CPython в обсуждениях исправления сомневались в необходимости отдельного автотеста: создать контролируемые условия с гарантированной нехваткой памяти в универсальном тесте довольно сложно.

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

Когда приложение действительно уязвимо

Уязвимость проявляется только в определённом шаблоне использования API:

- создаётся один экземпляр декомпрессора (`LZMADecompressor`, `BZ2Decompressor` или `GzipFile`),
- он используется многократно для последовательной обработки разных фрагментов данных,
- после возникновения ошибки `MemoryError` экземпляр не создаётся заново, а продолжается его повторное использование.

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

Упрощённый пример потенциально опасного кода:

```python
d = lzma.LZMADecompressor()

try:
d.decompress(malicious_data)
except MemoryError:
# Ошибку просто проглотили, объект оставили жить
pass

Повторное использование того же экземпляра

d.decompress(more_malicious_data)
```

Если атакующий контролирует и `malicious_data`, и `more_malicious_data`, а также может влиять на условия запуска (ограничения памяти процесса, окружение), у него появляется шанс эксплуатировать use-after-free.

Почему это так серьёзно оценено (9.1, Critical)

На первый взгляд требование искусственно спровоцировать `MemoryError` может показаться снижением практического риска. Но несколько факторов делают уязвимость критической:

- уязвим код из стандартной библиотеки, который используется повсеместно;
- модули lzma, bz2 и gzip часто применяются для распаковки данных, полученных извне;
- ошибка находится на стыке интерпретатора и C-библиотек, что традиционно делает эксплуатацию более реалистичной;
- потенциальный эффект - не только падение процесса, но и чтение конфиденциальных данных из памяти и выполнение произвольного кода.

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

CPython против "языка Python"

Важно чётко разделять язык Python и его конкретную реализацию. Уязвимость находится не в самом языке, а в реализации CPython - доминирующей, но не единственной. Python-экосистема включает несколько реализаций:

- CPython - "дефолтный" интерпретатор, написанный преимущественно на C;
- PyPy - альтернатива с JIT-компиляцией;
- Jython - реализация под JVM;
- IronPython - реализация для платформы .NET.

Проблема описана именно для CPython и его модулей `_bz2module`, `_lzmamodule` и `zlibmodule`. В других реализациях, где модули сжатия могут быть реализованы иначе или поверх других библиотек, наличие или отсутствие аналогичной уязвимости зависит от их собственного кода. Автоматически переносить CVE на все реализации Python нельзя - требуется отдельный анализ.

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

Роль низкоуровневого C-кода и "культ минимальных проверок"

Ситуация наглядно демонстрирует классическую проблему: когда высокоуровневый язык с богатой стандартной библиотекой упирается в необходимость иметь быстро работающие модули на C. В таких модулях:

- доступна ручная работа с памятью,
- требования к производительности традиционно высоки,
- возникает искушение минимизировать количество проверок и ветвлений ради скорости.

При этом "дополнительные проверки" часто воспринимаются как избыточный оверхед. Но с точки зрения безопасности корректный низкоуровневый код обязан содержать все необходимые проверки граничных условий и состояний, особенно при обработке ошибок. В данном случае именно неполная или некорректная обработка сценария с нехваткой памяти и привела к use-after-free.

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

Почему так сложно писать безопасный C-код

С языка C по-прежнему начинается большая часть низкоуровневой инфраструктуры, в том числе интерпретаторы, системные библиотеки и модули сжатия. Но:

- управление памятью целиком на совести разработчика;
- компилятор не предотвращает типовые ошибки типа use-after-free, double free или выход за границы буфера;
- любая забытая проверка возвращаемого значения или состояния структуры со временем превращается в потенциальную дыру в безопасности.

Создать по-настоящему безопасную и при этом производительную C-библиотеку - задача, требующая высокой квалификации и значительных ресурсов. Поэтому в реальных проектах часто побеждает компромисс: "достаточно хорошо, работает быстро". До тех пор, пока не появляется CVE.

Реальный риск эксплуатации

Чтобы эксплуатировать эту конкретную уязвимость, злоумышленнику нужно:

1. Иметь возможность передавать приложению сжатые данные в одном из затронутых форматов.
2. Знать или угадать, что приложение использует один и тот же объект-декомпрессор повторно.
3. Суметь создать условия, при которых распаковка первого фрагмента приведёт к `MemoryError`.
4. Грамотно подготовить второй фрагмент данных так, чтобы он использовал уже освобождённые структуры памяти в выгодной для атакующего конфигурации.

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

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

Как снизить риск до установки патча

До официального обновления интерпретатора и дистрибутивов можно предпринять несколько защитных мер на уровне кода:

1. Не переиспользовать декомпрессоры после ошибок.
После любого исключения при распаковке (особенно `MemoryError`) создавайте новый экземпляр `LZMADecompressor`, `BZ2Decompressor` или `GzipFile`. Самый простой и надёжный подход - использовать отдельный объект под каждую независимую операцию распаковки.

2. Жёстко валидировать входные данные.
Если возможно, ограничивайте максимальный размер входных архивов, не принимайте бесконечные или неподтверждённые по размеру потоки, используйте дополнительные уровни валидации.

3. Контролировать лимиты памяти и мониторинг.
В условиях ограниченной памяти следите за частотой появления `MemoryError`. Резкий рост таких ошибок может быть индикатором как атаки, так и логических проблем в коде.

4. Локализовать работу с подозрительными данными.
Интернет-данные, пользовательские загрузки и любые непроверенные архивы лучше обрабатывать в отдельных процессах или контейнерах с ограниченными правами, чтобы снизить последствия возможной эксплуатации.

Будут ли подобные ошибки исчезать?

Уязвимость в модулях сжатия CPython - не уникальный случай, а проявление системной реальности: пока стандартные библиотеки высокоуровневых языков опираются на ручной C-код, ошибки работы с памятью периодически будут всплывать. Это не делает язык Python "небезопасным по определению", но подчёркивает:

- важность строгих практик разработки для низкоуровневых модулей;
- необходимость регулярного аудита и тестирования сценариев обработки ошибок;
- полезность защитных механизмов на уровне компилятора и рантайма (санитайзеры, дополнительные проверки, опциональные режимы с усиленной безопасностью).

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

Итог

В CPython обнаружена серьёзная уязвимость в модулях распаковки lzma, bz2 и gzip, связанная с обращением к уже освобождённой памяти после `MemoryError`. Ошибка затрагивает сценарии, где один и тот же объект-декомпрессор повторно используется после неудачной распаковки. Потенциальные последствия - от утечки данных до выполнения произвольного кода.

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

Прокрутить вверх