Как str.lower() превратился в уязвимость Python

· Безопасность
Обычный вызов приведения строки к нижнему регистру нарушил соответствие стандарту IDNA 2003 и обернулся CVE. Причина — в версии Unicode, с которой работает интерпретатор.

cover.svg

Сет Ларсон, отвечающий за безопасность в Python Software Foundation, разобрал поучительный случай: безобидный на вид вызов str.lower() в стандартной библиотеке оказался уязвимостью, получившей идентификатор CVE-2026-17084.

Откуда растут корни

Многие интернет-стандарты исторически понимают только ASCII, тогда как мир пишет далеко не только латиницей. Чтобы сопоставить имена доменов из разных алфавитов с ASCII, придумали механизм IDNA. Его старая версия, IDNA 2003, опирается на алгоритм StringPrep (RFC 3454) и профиль NamePrep (RFC 3491). Python поддерживает её через кодек idna (str.encode('idna')), а более новую IDNA 2008 — через отдельный пакет idna с PyPI.

Один из шагов StringPrep — «case folding», то есть приведение символов к единому регистру для сравнения без учёта заглавных и строчных букв. Реализация в модуле stringprep для символов, не попавших в таблицу исключений, просто вызывала code.lower().

В чём подвох

Проблема в том, что str.lower() использует ту версию базы Unicode, с которой собран конкретный интерпретатор. Узнать её можно через unicodedata.unidata_version — в свежих сборках это уже 17.0.0. Между тем StringPrep жёстко привязан к правилам Unicode 3.2.0: таблицы B.2 и B.3 из RFC 3454 — это, по сути, правила регистра именно той версии, зафиксированные вручную.

Расхождение между реализацией и спецификацией и есть уязвимость. Ларсон приводит наглядный пример: строка из двух символов «ᎠᎠ» (U+13A0, из алфавита чероки) по стандарту должна кодироваться как xn--58da, но при использовании правил Unicode 17.0.0 даёт совсем другой результат — xn--kz9aa. Для механизма, который решает, какому домену соответствует введённое имя, такое расхождение — прямой путь к путанице и потенциальным атакам.

Как починили

Лекарство оказалось прямолинейным: разработчики прошлись по всем кодовым точкам Unicode и записали в отдельную таблицу исключений те случаи, где поведение str.lower() в текущей версии Python отличается от Unicode 3.2.0. Теперь функция ведёт себя так, будто пользуется старой базой, и IDNA 2003 снова соответствует стандарту.

Ларсон благодарит Bitshift за сообщение об уязвимости, Стэна Ульбрыха за совместную разработку исправления, а Марка-Андре Лембурга и Петра Викторина — за ревью. Его работа в PSF финансируется инициативой Alpha-Omega.

Мораль истории проста: там, где стандарт требует зафиксированной версии Unicode, «обычное» приведение к нижнему регистру перестаёт быть обычным.


Источник: Lobsters

Комментарии

Войдите, чтобы комментировать.

  • Пока нет комментариев.