ReDoS-атаки: как одна регулярка может положить сервер

Представьте: один HTTP-запрос к серверу с особым текстом в поле логина — и ваш веб-сервер зависает на минуты, потребляя 100% CPU. Без ботнета, без DDoS-армии, без вредоносного ПО. Просто строка из десятка символов, которая триггерит экспоненциальное время работы регулярного выражения. Это и есть ReDoS (Regular expression Denial of Service).

По данным OWASP, уязвимости типа ReDoS входят в категорию A03 (Injection) и встречаются в валидации email, URL, паролей, IP-адресов — буквально в любой веб-форме. В этой статье мы разберём механику атаки, примеры уязвимых паттернов, актуальные CVE и методы защиты для самых популярных языков и платформ.

ReDoS-атака: сервер опутан цепями из символов регулярных выражений

⚙ Как работает ReDoS

ReDoS эксплуатирует механизм backtracking (возврата) в движках регулярных выражлений. Когда regex-движок встречает неоднозначный паттерн — например, (a+)+b — и входная строка не соответствует шаблону, он начинает перебирать все возможные комбинации разбиения строки. В худшем случае количество комбинаций растёт экспоненциально от длины входа.

⚠ Суть проблемы: строка из 30 символов может заставить regex-движок выполнить триллионы операций backtracking. На практике это означает 100% загрузку CPU на несколько минут — или часов, если строка длиннее.

Классический пример: паттерн ^(a+)+$ со входом "aaaaaaaaaaaaaaaaaaaaaaaaaaaaac". Эту строку чаще всего генерируют через a * N + "c". При N=25 движок делает более 67 миллионов проверок. При N=30 — уже миллиарды.

Три условия для ReDoS:

  1. В регулярном выражении есть повторяющаяся группа с повторителем (например, (a+)+, (.*)*)
  2. Между этими повторами есть пересечение — одно и то же множество символов может быть захвачено разными частями паттерна
  3. Входная строка не совпадает с паттерном, но почти совпадает — это запускает полный перебор всех вариантов

Вот как это выглядит в коде:

// Python — уязвимый regex
import re, time
pattern = re.compile(r'^(a+)+$')
start = time.time()
pattern.match("a" * 28 + "c")  # 28 'a' + 'c'
print(f"Time: {time.time() - start:.2f}s")
# Результат: 15+ секунд, 100% CPU

🔍 Уязвимые паттерны регулярных выражений

Не все регулярки уязвимы. Проблема возникает, когда внутри одного выражения встречаются вложенные квантификаторы. Вот типичные опасные паттерны:

ПаттернРискПример
(a+)+ Критический Любая валидация строк с повторами
(.*)* Критический Валидация URL, email
(a|aa)+ Высокий Паттерны с альтернативами внутри повторов
(a|ab)*b Высокий Пересекающиеся альтернативы
\s+[\w-]+ Средний Разбор текста с пробелами
^(\w+\s?)+\w+$ Высокий Валидация многословных полей

Особенно опасны регулярки, которые проходят через внешний ввод — валидация форм, URL-parameters, заголовки HTTP. В 80% случаев ReDoS-уязвимости обнаруживаются именно там.

⚠ Важно: ReDoS — это не абстрактная теория. Уязвимости этого типа находили в модулях validator.js (один из самых популярных npm-пакетов), urllib в Python, java.net.URL и даже в библиотеках белых хакеров типа sqlmap. Правильная подготовка позволяет снизить риск атаки почти до нуля — современные инструменты статического анализа находят 90% уязвимых паттернов до деплоя.

📄 Реальные CVE и инциденты

ReDoS — не гипотетическая угроза. Вот несколько подтверждённых уязвимостей в широко используемых библиотеках:

Во всех случаях атакующий мог положить сервер одним запросом. Критично, что многие из этих CVE находились в de facto стандартных библиотеках, которые используются в production миллионами проектов.

💻 ReDoS по языкам: где уязвимо, а где нет

Разные regex-движки по-разному устойчивы к ReDoS. Важно понимать, какой движок использует ваш стек:

JavaScript / Node.js

Движок V8 использует NFA-бэктрэкинг (стандартный backtracking). Node.js — самая уязвимая платформа к ReDoS. В 2025-2026 годах было опубликовано более десятка CVE на npm-пакеты. Защита: использовать re2 (библиотека Google) вместо нативного RegExp — она использует детерминированный автомат без backtracking. Либо ставить --enable-experimental-regexp-engine в V8 (доступно с Node 22+).

// Node.js — переключение на re2
const RE2 = require('re2');
const safeRegex = new RE2('^(a+)+$');
// re2 выбросит исключение, если паттерн потенциально опасен

Python (re модуль)

Стандартный re — также NFA с backtracking, уязвим. Python 3.11+ имеет re.timeout (через signal), но это только защита от зависания, не от потребления CPU. Рекомендация: использовать regex (сторонний модуль с таймаутами) или re.compile() с ручным таймаутом через concurrent.futures. Регулярно проверяйте regex на сайте regex101.com с тестовыми длинными строками.

# Python — защита через таймаут
from concurrent.futures import ThreadPoolExecutor, TimeoutError

def safe_match(pattern, text, timeout=2):
    with ThreadPoolExecutor() as executor:
        future = executor.submit(pattern.match, text)
        try:
            return future.result(timeout=timeout)
        except TimeoutError:
            print("ReDoS detected! Aborting.")
            return None

Java (java.util.regex)

Java — уязвима, но с опцией Pattern.compile(regex, Pattern.UNIX_LINES) и ручными лимитами через Thread.setUncaughtExceptionHandler. В Java 21+ есть Java Flight Recorder для детекции длинных regex. Лучшая практика: использовать com.google.re2j (Java-порт re2 от Google).

.NET (C#)

В .NET начиная с .NET 7 добавлен Regex.EnumerateMatches с небэктрэкинговой реализацией. Также есть Regex.InfiniteMatchTimeout и возможность задать глобальный таймаут через AppContext. .NET — наиболее защищённая платформа из популярных.

Код с уязвимым регулярным выражением и защитные меры

🔑 Методы защиты от ReDoS

Полная защита от ReDoS строится на трёх уровнях: на уровне кода, на уровне инфраструктуры и на уровне процессов разработки.

1. Используйте re2 вместо нативного RegExp

Google re2 — это библиотека регулярных выражений, которая гарантирует линейное время выполнения независимо от паттерна и входных данных. Она доступна для C++, Go, Node.js, Python (google-re2), Java (re2j). Единственное ограничение: re2 не поддерживает backreferences и lookahead/lookbehind, но 95% продуктивных regex не используют эти фичи.

2. Установите таймауты на все regex-операции

Даже если у вас нет возможности заменить движок, установите жёсткий таймаут на каждое исполнение регулярного выражения. В Node.js используйте --regexp-timeout=N (флаг V8, доступен с Node 20+). Таймаут должен быть не более 1-2 секунд.

3. Статический анализ regex при коммите

Используйте инструменты статического анализа, которые находят уязвимые паттерны до попадания в production:

4. Check-list безопасности

✅ Чек-лист защиты от ReDoS:
☑ Замените нативный RegExp на re2 (Node.js, Python, Java)
☑ Установите таймаут на regex-операции (не более 2 секунд)
☑ Добавьте CodeQL/Semgrep-скан в CI/CD пайплайн
☑ Ограничьте длину строк на входе (например, max 256 символов)
☑ Не используйте new RegExp(userInput) — это опасно вдвойне
☑ Обновите зависимости: проверьте CVE для используемых regex-библиотек

5. Ограничение длины входных данных

Простое, но эффективное правило: ограничьте длину строк, проходящих через regex, до 200-500 символов. Большинству валидаций (email, URL, телефон) не нужно больше. Это не устраняет ReDoS полностью, но экспоненциальный рост при коротких строках даёт лишь доли секунды торможения вместо минут простоя.

🔨 Инструменты для поиска уязвимых regex

Перед деплоем проверьте свои регулярные выражения с помощью этих инструментов:

📝 Вывод: ReDoS — это реальная угроза, но правильный подход к написанию и проверке регулярных выражений снижает риск атаки практически до нуля. Используйте re2, устанавливайте таймауты, тестируйте regex на длинных строках и сканируйте код в CI/CD. Современные инструменты находят 90% уязвимых паттернов автоматически — внедрите их в свой пайплайн уже сегодня.

🔗 Источники

📚 Читайте также

📖 В начало