Всем привет, меня зовут Семён. Я пишу на С++ и работаю в группе Антиробота. Антиробот — это сервис, который на уровне L7 защищает нас от парсеров и DDoS-атак. Разрабатывать его начали более 10 лет назад — сначала он предназначался только для защиты Поиска, затем был внутренним инструментом, который в онлайн‑режиме анализирует запросы к сервисам Яндекса. Постепенно Антиробот вырос в настоящий highload. Сейчас это часть облачного сервиса Smart Web Security (SWS).

В этой статье я расскажу, как с нашим сервисом мы прошли путь от текстовых правил до машинного обучения. Вы узнаете, зачем вообще нужен Web Application Firewall (WAF) — межсетевой экран для веб-приложений — и разберётесь, как он устроен. А ещё — как работают рулсеты, почему у нас их целых три и какие существуют метрики для оценки качества и быстродействия сервиса.

Зачем нужен WAF

Межсетевой экран для веб-приложений защищает сервисы от эксплуатации и попыток эксплуатации уязвимостей на уровне L7. Без WAF может случиться утечка пользовательских данных, отказ обслуживания сервиса, а нестабильно или некорректно работающий сайт может привести к остановке бизнеса. Если сканеры уязвимостей обращаются к вашим бэкендам с большим RPS, это может вызывать повышенную нагрузку на них.

Что лучше использовать для разработки WAF: машинное обучение или текстовые правила? Кажется, что в 2025 году ответ на этот вопрос очевиден. Плюсы ML понятны. Такие решения могут, например, отражать атаки, сигнатуры которых ещё не известны специалистам по ИБ. Преимущества текстовых же правил в том, что они являются более понятными и интерпретируемыми.

Однако на практике у каждого метода также есть свои ограничения. Чтобы лучше понять риски использования ML и текстовых правил, посмотрим подробнее, как это работает.

Интерфейс решения

Первое, что встречает пользователей при настройке WAF, — это WAF-профиль.

Здесь настраивается, какие рулсеты использовать, где можно включать-выключать правила в рулсетах, настраивать пороги аномальности и так далее.

Ruleset (рулсет) — это набор правил, которые объединены некоторой парадигмой.

В Smart Web Security у нас есть три таких набора. Это OWASP® Core Ruleset,
Yandex Ruleset с текстовыми правилами и Yandex Malicious Score, основанный на машинном обучении.

Как происходит блокировка запроса? Чтобы разобраться в этом, стоит рассказать о некоторых абстракциях интерфейса.

  1. Группы правил. Все правила в каждом наборе от Яндекса объединены в группы по атакам, которые они отражают. Например, есть группы правил SQL-инъекций и RCE.

  2. Балл аномальности. Каждое правило имеет свой вес — иными словами, аномальность. Для любого сработавшего правила аномальность добавляется в общую сумму группы. Например, если в группе сработало три правила с весом 1, то общая сумма для группы будет 3.

  3. Правила-исключения, которые позволяют отключить часть правил или все, если трафик соответствует определённым условиям. Можно, например, отключить часть правил для конкретной API-ручки.

Также в одном профиле WAF можно использовать несколько рулсетов и ожидать вердикта блокировки, как от всех сразу, так и от одного из них. Если рулсетов в профиле несколько, то мы применяем к их вердиктам операторы «И»/«ИЛИ».

Зачем это делать? Есть три сценария:

  • Мы хотим уменьшить вероятность ложной блокировки: объединяем рулсеты через «И» и ждём вердикт блокировки от всех.

  • Мы хотим увеличить покрытие атак: ждём блокировку от одного из них («ИЛИ»).

  • Мы объединяем некоторые функциональные особенности рулсетов.

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

Соответственно, если в рулсете что-то из этого сработало — балл аномальности или блокирующее правило, — то мы либо ждём блокировки от всех рулсетов в случае оператора «И», либо сразу баним запрос.

Архитектура сервиса

Теперь покажем, как архитектурно устроена защита на уровне L7. Мы одновременно рассмотрим и устройство Антиробота как внутреннего сервиса, и его работу в составе облачного сервиса SWS, так как основные принципы будут те же.

Примерно так выглядит запрос от клиента до бэкенда в современных приложениях.

Он проходит через L7-балансировщик, который проксирует его до бэкенда. Место Антиробота в данном случае находится после балансировщика. Балансер фактически спрашивает Антиробота, может ли он пропустить запрос до бэкенда.

В свою очередь, Антиробот в кейсе с WAF ходит в него по сети через L3-балансер, отдаёт ему запрос, и L7-файрвол отвечает вердиктом в баллах (score) по правилам, сработали какие-то из них или нет.

Также Антиробот периодически ходит в API, где лежат клиентские настройки. Дальше он совмещает клиентские настройки и вердикт файрвола и отдаёт своё решение на L7-балансировщик.

Однако рулсеты работают немного по-разному. Yandex Ruleset и OWASP® Core Ruleset работают на отдельных серверах, и мы ходим туда по сети. Yandex Malicious Score работает на серверах Антиробота.

Почему так? Если мы деплоим на отдельных серверах, то получаем дефолтные плюсы от микросервисов. Это, например, упрощение развёртывания, скейлинга, независимость технологий и упрощение разработки. Однако Yandex Malicious Score работает на тех же машинах для уменьшения таймингов с десятков миллисекунд для похода по сети до сотен микросекунд. Это позволяет безболезненно включить этот сервис для бэкендов внутри Яндекса или, например, для ваших бэкендов.

У Yandex Ruleset и Yandex Malicious Score единый интерфейс, что позволяет в любой момент вынести Yandex Malicious Score как отдельный сервис и ходить в него по сети. Таким образом мы получаем какие-то плюсы, например, упрощение разработки из-за того, что Yandex Malicious Score фактически — это отдельная библиотека в Антироботе.

Как работает Yandex Ruleset

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

SecRule RESPONSE_BODY "@rx (?i:JET Database Engine|Access Database Engine|\[Microsoft\]\[ODBC Microsoft Access Driver\])"
 "id:951110,
 phase:4,
 block,
 capture,
 t:none,
 msg:'Microsoft Access SQL Information Leakage',
 tag:'application-multi',
 setvar:'tx.sql_injection_score=+%{tx.critical_anomaly_score}'"

В примере на скрине мы берём response_body, мэтчим его регуляркой, и если она подходит, то блокируем запрос. При этом запрос блокируется не сразу: мы добавляем к итоговой аномальности некий балл, который зависит от severity правила. Клиенты по аналогии также сами могут настроить порог аномальности, при которой запрос блокируется.

Что насчёт Yandex Malicious Score

Yandex Malicious Score, в свою очередь, работает немного по-другому: он берёт сырой HTTP- запрос, парсит его на разные сущности — хедеры, имена хедеров, куки и так далее; нормализует, убирает все кодировки, чтобы вердикт был более точным и мы могли обучать ML-модели без кодировок. Дальше происходит векторизация. Мы получаем по вектору чисел для каждой нормализованной сущности и над каждым вектором запускаем CatBoost.

Выбираем максимумы для каждого класса атаки, сравниваем с настройками и отдаём вердикт на L7-балансировщик. На выходе после запуска модели мы получаем вектор баллов по разным классам атак.

Посмотрим пример работы. Отправим такой запрос.

headers = {
    "X-Custom-Key": '/prompt.call(null,1);’
}
response = requests.get(url, headers=headers)
print(response.status_code)

Тут в хедерах есть плохая нагрузка, поэтому мы получим ошибку 403.

Можно посмотреть, что случилось, в логах сервиса.

{
    "is_blocking_rule": "false", 
    "is_dry_run_rule": "false", 
    "matched_data_key": "x-custom-key", 
    "matched_data_value": "/prompt.call(null,1);", 
    "matched_data_variable": "REQUEST_HEADERS", 
    "rule_group_id": "yams-latest-attack-malicious", 
    "rule_id": "yams-latest-id8200601-attack-malicious", 
    "rule_set_type": "Yandex ML Ruleset", 
    "rule_set_version": "latest", 
    "score": "88", 
    "sws_profile_id": "esqk4g45eq0c7t2apbj1", 
    "sws_profile_rule_name": "mlwaf", 
    "unique_key": "7108958864342515820", 
    "verdict": "DENY", 
    "waf_profile_id": "esqi08bh5jccrj455llb" 
}

Запрос забанил Yandex ML Ruleset и отправил пользователя именно на 403. Видно, какие правила сработали и к какой группе это правило относится. И самое главное, видно, где находится атака, по какому она находится ключу, саму атаку и балл, который поставил ML WAF в этой нагрузке.

Архитектура ML-модели

Как устроена ML-модель и как её учили

Мы используем CatBoost на основе float-фич. Векторизацию мы сделали с помощью словаря N-грамм. Модель, в свою очередь, решает задачу мультитаргетной классификации.

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

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

Метрики качества

Цель по метрикам качества — держать TNR 99% на легитимных запросах, а также постоянно растить recall. Однако, как и в любой задаче антифрода, создание хорошей выборки — довольно сложный процесс. Из-за волатильности среды, постоянного появления новых атак подходящий датасет создать довольно тяжело.

Как мы можем улучшать эти метрики качества? Для Yandex Ruleset мы можем это делать с помощью добавления исключений, которые запрещают работу конкретных правил на определённых сущностях. Также можем дописывать правила и улучшать существующие.

Для Yandex Malicious Score можем дополнять данные для обучения, а также лучше выбирать параметры для модели и улучшать процесс выбора N-грамм.

Существуют не только метрики качества, такие как TNR и recall, ещё есть регрессионные тесты и сканеры уязвимостей. В дополнение можем смотреть на конкурентов и улучшать скорость модели.

Метрики быстродействия Yandex Malicious Score

Когда мы сделали ML WAF, поняли, что он работает довольно медленно в первую очередь из-за того, что это машинное обучение. У него довольно большое время ответа, порядка Антиробота — 2–3 мс. Также компонент предъявляет довольно большие требования по железу. Мы поняли, что это нужно улучшать.

Для решения проблемы мы использовали сервис для проведения нагрузочного тестирования Yandex Load Testing, который есть в Yandex Cloud. В нём можно измерить максимальный RPS на сервис и посмотреть, как он работает под нагрузкой. Также нам пригодился Perforator — наша система непрерывного профилирования, которая вышла недавно в опенсорс. Она позволяет снимать флейм-графы (Flame Graphs) с проектов на С++ и не только, чтобы понять, где ваш код тормозит.

Начинали мы примерно с такой картины: в среднем сервис отвечал за 2 мс, что довольно долго.

Сначала стали оптимизировать CatBoost. Изначально мы запускали CatBoost отдельно для каждой сущности, то есть на выходе после векторизации получали М сущностей и М раз запускали CatBoost.

Мы перешли на обсчёт матриц. То есть все векторы, получившиеся из M нормализованных сущностей после векторизации, объединили в матрицу {М х на количество фич} и выиграли за счёт этого примерно 20% CPU.

Дальше мы сделали кеш. Малое количество сущностей в интернете и в целом в текстах довольно часто повторяется. Этот кеш может быть довольно малого размера, он может быть не согласованным на разных виртуальных машинах и его не нужно инвалидировать. Получили Cache Hit порядка 10%.

Также мы занимались оптимизацией векторизации. Изначально переход от текста к векторам делали с помощью словаря N-грамм. Значительное время в этой функции занимало то, что мы идём в этот словарь, делаем там lookup и считаем хеш и equals-процедуру. Мы переписали это на идеальное хеширование и полностью переписали алгоритм на явные AVX2-инструкции. Получили выигрыш порядка 15% CPU.

Из минусов: теперь наш код выглядит примерно вот так.

for (size_t i = 0; i + 7 < cnt; i += 8) {
    __m256 vectorRegister = _mm256_load_ps(vector + i);
    vectorRegister = _mm256_mul_ps(vectorRegister, mulConstRegister);
    _mm256_store_ps(vector + i, vectorRegister);
}

Но в целом это довольно поддерживаемо, так как находится отдельно от основного кода и хорошо задокументировано.

Что ещё можно сделать? Мы дополнили процесс принятия решения случаями, когда машинное обучение можно вообще не запускать. Получили довольно хороший результат.

В 78% случаев мы вообще не запускаем машинное обучение.

В итоге пришли к тому, что в среднем ML WAF отвечает за 300 мкс. Мы в 6 раз уменьшили медиану и на яндексовом трафике сохранили примерно 20 лет процессорного времени в день. По производительности на уровне мировых аналогов, что не может не радовать.

Риски и ограничения ML и текстовых правил

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

У Yandex Ruleset ограничения тоже понятны. Он не может автоматически подстраиваться под новые атаки, в отличие от машинного обучения, потому что это статический набор правил. Кроме того, в полноте он ограничен экспертизой человека.

Использование трёх наборов правил помогает нам обойти ограничения каждого из них и при этом объединить разные возможности.

  • Yandex Malicious Score работает довольно быстро, подстраивается под новые атаки, потому что основан на машинном обучении и постоянно обучается. Его довольно просто настраивать, фактически его нужно просто включить.

  • Yandex Ruleset предсказуем, так как написан на текстовых правилах. Эксперты постоянно эти правила дописывают, и его тоже довольно просто настроить, так же как и ML WAF.

  • OWASP CRS тоже предсказуем, так как написан в текстовых правилах. У него есть плюс — то, что он есть в опенсорсе и постоянно обновляется комьюнити. Мы, со своей стороны, постоянно подтягиваем новые версии рулсета к себе, но его чуть сложнее настраивать. Надо включать на высокий уровень Paranoia level, работать с false-positive и так далее. Также скорее всего для его настройки понадобится ИБ-специалист.

Пользователи Антиробота могут использовать все три рулсета, чтобы увеличить покрытие атак, или же настраивать разные комбинации использования правил, чтобы адаптировать защиту к специфическим угрозам.

При этом важно помнить, что WAF — это элемент комплексной защиты, которая также должна включать дополнительные усилия:

  • безопасную разработку фронтенда и бэкенда;

  • контроль и валидацию на стороне бэкендов;

  • регулярный аудит/пентесты/bugbounty;

и так далее.

Как и в случае других средств защиты, в условиях постоянного появления новых угроз вряд ли возможна ситуация «один раз настроил WAF и забыл». Но когда у пользователя есть несколько способов настройки, это помогает лучше адаптироваться к изменениям ИБ-ландшафта.

Комментарии (2)


  1. achekalin
    21.11.2025 14:23

    Антиробот — это сервис, который на уровне L7 защищает нас от парсеров и DDoS-атак. Разрабатывать его начали более 10 лет назад

    Скажите, я вот слышал от многих людей, что РСЯ скликивание показов ботами прямо очень распространено, и Я ничего не делает с этим - а у Вас описание технологии, которой более 10 лет, почему ейюё не прикрутили?


    1. 000111
      21.11.2025 14:23

      Интересный вопрос, даже достаточно риторический, но не по адресу.