
Обнаруживает паттерн rewrite CVE-2026-42945 в конфигурациях nginx: rewrite с ? в строке замены плюс безымянный захват, используемый в том же location.
Находит конфигурации, уязвимые к CVE-2026-42945 — переполнению буфера в куче в ngx_http_rewrite_module nginx.
Запуск уязвимой версии nginx (с 0.6.27 по 1.30.0) сам по себе не делает конфигурацию эксплуатируемой. Конфигурация должна содержать конкретную пару директив, и такая пара редка: два независимых сканирования реальных конфигураций нашли ноль эксплуатируемых из 1465 и одну из 35633. Этот скрипт показывает, по какую сторону этой границы находитесь вы.
Директиву rewrite, в замене которой после ? идут аргументы, за которой следует безымянный захват ($1–$9), читаемый в том же location:
location / {
rewrite ^(.*) /new?c=1; # the ? here raises the internal is_args flag
set $myvar $1; # ...which was never cleared, and leaks into this
return 200 $myvar;
}
Один проход вычисляет размер буфера для исходной строки, следующий копирует в него экранированную версию. Обе половины по отдельности выглядят корректно.
Каждое правило ниже было измерено на nginx 1.30.0, а не выведено умозрительно, потому что в опубликованных описаниях два из них изложены неверно.
Два следствия, которые стоит назвать явно:
Завершающий ? не опасен. /new/$1? — это способ отбросить унаследованную строку запроса, и он встречается в значительной доле миграционных конфигураций. Если помечать каждый ?, можно было бы закричать на пол-интернета.
Утверждение «именованные захваты безопасны» неверно. Важно, как захват читается, а не как он объявлен. Именованная группа, прочитанная как $1, падает точно так же, как и безымянная.
По тем же измеренным причинам игнорируются: ? внутри самого регулярного выражения (квантификатор), а также redirect / permanent / break, которые завершают обработку до того, как сработает потребитель.
nginx -T | python3 check_rewrite.py
python3 check_rewrite.py /path/to/dump.txt
python3 check_rewrite.py --json /path/to/dump.txt
Только Python 3, только стандартная библиотека.
Коды возврата: 0 — чисто, 1 — пара найдена, 2 — конфигурацию не удалось прочитать или разобрать. Смысл в третьем: незакрытая кавычка или несбалансированные скобки обрезают анализ, а проверяльщик, отвечающий «ничего не найдено» для файла, который он не смог прочитать, хуже того, который падает. В таком случае он сообщает об этом и возвращает 2.
Передавайте ему вывод nginx -T, а не отдельные файлы: include-файлы могут находиться где угодно, а собранная конфигурация достоверна только на работающем сервере. Находки сообщаются с указанием файла и строки, где директива действительно находится, а не смещения в дампе.
$ python3 check_rewrite.py samples/vuln-nginx-T.txt
[HIGH] находка #1
location: location / (/etc/nginx/nginx.conf:14)
rewrite: rewrite ^(.*) /new?c=1 (/etc/nginx/nginx.conf:15)
захват $N: set -> set $myvar $1 (/etc/nginx/nginx.conf:16)
почему: обработка продолжается в этом же location без редиректа
Итого находок: 1
Они также печатаются в конце каждого отчёта, чтобы никто не принимал инструмент за гарантию:
location;rewrite и set, расположенные непосредственно в server, а не внутри location, не отслеживаются;set $tmp $1; с последующим использованием $tmp) не прослеживаются;try_files, error_page и именованные location не прослеживаются;if, который никогда не срабатывает, всё равно будет сообщена.Обновление надёжнее любой проверки конфигурации. Возьмите текущий стабильный или mainline-релиз с nginx.org.
python3 -m unittest test_check_rewrite -v
29 тестов. Большинство из них — негативные случаи, поскольку в таком инструменте главный риск — это кричать на конфигурации, с которыми всё в порядке. Он также запускался на продакшн-конфигурации из 250 строк в одиннадцати подключаемых файлах, с регулярным выражением в map и CSP-заголовком, полным точек с запятой внутри кавычек: ноль находок, ноль жалоб при разборе и ровно одна находка после внедрения в неё настоящей пары.
MIT
| Конфигурация внутри одного location | Падение |
|---|
rewrite ^(.*) /new?c=1; + set $x $1; | да |
rewrite ^/old/(.*)$ /new?x=1; + set $x $1; | да |
rewrite ^/old/(.*)$ /new/$1?; + set $x $1; | нет |
rewrite ... /mid?c=1; затем rewrite ... /new; затем set $x $1; | нет |
rewrite ^(.*) /new?c=1 break; + set $x $1; | нет |
rewrite ^(?<tail>.*) /new?c=1; + set $x $1; | да |
rewrite ^(?<tail>.*) /new?c=1; + set $x $tail; | нет |