В Python нашли уязвимость CVE-2026-17084: str.lower() портит кодирование доменных имён

Автор материала, разработчик, работающий Security Developer-in-Residence в Python Software Foundation (работа спонсируется Alpha-Omega), разобрал, почему в стандартной библиотеке Python обнаружилась уязвимость на стыке двух старых интернет-стандартов.

Для работы с доменными именами, где используются не только латинские буквы, нужен способ привести Unicode-строку к ASCII. Для этого создан StringPrep (RFC 3454), а его профиль NamePrep (RFC 3491) лежит в основе IDNA 2003, более старой из двух версий стандарта интернационализированных доменных имён (её сменила IDNA 2008, RFC 5890, 5893). В Python IDNA 2003 реализована через кодек idna (вызов str.encode('idna')), а IDNA 2008, через отдельный пакет idna с PyPI; автор отмечает, что в общем случае стоит использовать именно пакет idna (IDNA 2008), а не .encode('idna') (IDNA 2003), но иногда нужно именно старое поведение.

Внутри StringPrep есть шаг case folding, приведение регистра символов по таблицам B.2 и B.3 из RFC 3454. Таблица B.2 в реализации Python, это фактически str.lower(), а B.3 задаёт исключения. Проблема в том, что str.lower() в Python работает по той версии базы Unicode, что зашита в конкретную сборку интерпретатора (её можно узнать через unicodedata.unidata_version, например «17.0.0»), тогда как StringPrep и IDNA 2003 жёстко привязаны к правилам регистронезависимого сравнения именно версии Unicode 3.2.0, она отдельно доступна в Python через unicodedata.ucd_3_2_0 и явно используется в модулях Lib/stringprep.py и Lib/encodings/idna.py.

Из-за этого расхождения одна и та же строка может кодироваться по-разному в зависимости от того, какая версия Unicode пришла с интерпретатором. Автор приводит пример: строка «ᎠᎠ» (символ U+13A0) по спецификации (по правилам Unicode 3.2.0) должна кодироваться как xn--58da, но при использовании case-folding по Unicode 17.0.0 через обычный str.lower() получается другое значение, xn--kz9aa. Это и есть суть уязвимости: реализация расходится со спецификацией, а расхождение реализации и спецификации в кодировании доменных имён, уже проблема безопасности.

Исправление, по описанию автора, заключается в добавлении новых исключений в таблицу B.3, так что для этой конкретной функции str.lower() теперь ведёт себя так, будто по-прежнему работает с Unicode 3.2.0, независимо от того, какая версия Unicode установлена в интерпретаторе. После этого исправления реализация IDNA 2003 в Python снова соответствует спецификации. Автор благодарит исследователя под ником Bitshift за обнаружение уязвимости, Stan Ulbrych, за совместную разработку исправления, а Marc-Andre Lemburg и Petr Viktorin, за ревью. Уязвимости присвоен идентификатор CVE-2026-17084.

Ключевые факты

  • Case-folding (приведение регистра) для IDNA 2003 в стандартной библиотеке Python реализован через str.lower(), который зависит от версии Unicode конкретного интерпретатора.
  • Спецификация StringPrep/IDNA 2003 требует правил регистронезависимого сравнения именно версии Unicode 3.2.0, а не текущей версии Unicode.
  • На примере строки «ᎠᎠ» показано расхождение: правильная (по спецификации) кодировка xn--58da против ошибочной xn--kz9aa при case-folding по Unicode 17.0.0.
  • Уязвимости присвоен идентификатор CVE-2026-17084; исправление добавляет исключения в таблицу B.3, чтобы str.lower() в этом месте вёл себя как под Unicode 3.2.0.
  • Автор, Security Developer-in-Residence в Python Software Foundation, работа спонсируется Alpha-Omega; в устранении участвовали исследователь Bitshift, Stan Ulbrych и рецензенты Marc-Andre Lemburg и Petr Viktorin.

Почему это важно

Уязвимость показывает конкретный механизм расхождения реализации со спецификацией: код, который выглядит безобидно (code.lower() в функции приведения регистра), на деле привязан к версии Unicode, зашитой в конкретную сборку интерпретатора, тогда как стандарт StringPrep/IDNA 2003 жёстко фиксирует версию 3.2.0. Итог, одна и та же Unicode-строка может кодироваться в разные ASCII-представления домена в зависимости от того, на какой версии Python и с какой версией Unicode она обрабатывается, что и было официально признано уязвимостью (CVE-2026-17084).

Кому это важно

В первую очередь, разработчикам на Python, чей код использует старую кодировку IDNA 2003 через str.encode('idna') (а не более новый пакет idna для IDNA 2008), а также авторам библиотек и инфраструктурного ПО, где сравнение или нормализация доменных имён завязаны на стандартную библиотеку. В более широком смысле это касается всех, кто поддерживает или разбирает код модулей stringprep и encodings/idna в самом Python, и сообщества безопасности CPython/PSF в целом.

Как это применить

Автор материала прямо советует: в общем случае предпочитать пакет idna с PyPI (реализация современного IDNA 2008), а не встроенный вызов .encode('idna') (IDNA 2003), последний стоит использовать только тогда, когда старое поведение нужно намеренно. Для тех, кому всё же нужен именно IDNA 2003, практический шаг, обновиться на версию Python, где применено описанное исправление таблицы B.3, чтобы str.lower() в этом месте снова соответствовал зафиксированным правилам Unicode 3.2.0, а не версии Unicode текущего интерпретатора.

Можно ли доверять

Материал написан человеком, занимающим официальную позицию Security Developer-in-Residence в Python Software Foundation (работа финансируется Alpha-Omega), с разбором на уровне исходного кода стандартной библиотеки, конкретными командами и воспроизводимым примером в интерпретаторе, а также с присвоенным CVE-номером и поимённой благодарностью нашедшему уязвимость и рецензентам исправления, это техническая заметка от вовлечённого специалиста, а не пересказ третьих лиц. При этом текст не указывает независимую сторону, подтвердившую исправление, кроме названных в тексте рецензентов.

Риски и подводные камни

В самом материале не указано, в какой именно версии (версиях) Python это исправление уже выпущено или будет выпущено, а также нет даты обнаружения проблемы или даты релиза с фиксом, то есть неясно, у кого конкретно и когда обновление уже доступно. Текст также не сообщает, эксплуатировалась ли уязвимость на практике до исправления, и не раскрывает деталей процесса разбора инцидента внутри PSF.