Хрю, инженерное сообщество! Пигмен у клавиатуры. Сегодня разбираем охранника, который так усердно защищает магазин, что покупатель не может обновить корзину. Чат тоже лёг. Зато ответ 403 работает безупречно. Знакомься: ложные срабатывания WAF на примере OWASP Coraza и Juice Shop.

Сразу расставлю копыта: этот стенд собрал автор лабораторного разбора, а не я. Он взял Coraza с OWASP CRS, Caddy и намеренно уязвимый Juice Shop. Я проведу тебя через пять срабатываний: где политика разошлась с логикой приложения, как сужали исключения и почему работающая корзина ещё не означает, что сайт безопасен.

Покупатель пришёл за товаром. А получил подозрение

План эксперимента был скромным: Python-скрипт с Playwright входит в магазин, ищет товар, добавляет его в корзину и доходит до оформления заказа. В конфигурации без пользовательских исключений WAF остановил часть этих действий. До оплаты ещё далеко, а приключения уже начались.

Коварство SPA в том, что интерфейс может выглядеть вполне прилично. Страница нарисовалась, кнопочки на месте, дизайнер спит спокойно. А фоновый запрос получил 403 — и нужное действие не выполняется. Поэтому я бы смотрел на весь сценарий целиком. Факт «главная открылась» здесь доказывает лишь то, что главная открылась.

CRS ищет признаки подозрительных запросов, но не читает мысли разработчика твоего магазина. Для API метод PUT может быть обычным обновлением данных. Для настроенной политики — методом за пределами разрешённого списка. Обе стороны делают свою работу. Объяснять их друг другу придётся инженеру.

949110 — это приговор. Доставай предыдущие строки

. audit.log. В конце цепочки часто стоит 949110, или Inbound Anomaly Score Exceeded. Рука уже тянется отключить виновника? Придержи копыто. Это правило сообщает о набранных баллах, а конкретные причины могли обнаружить другие проверки выше.

В приведённой конфигурации с Paranoia Level 1 порог составлял 5 баллов. Срабатывание CRITICAL добавляло 5, WARNING — 3. Баллы суммируются: убрал одну причину, но оставил остальные — запрос всё ещё может набрать достаточно для блокировки. Арифметика неумолима, даже когда очень хочется закрыть тикет.

Автор смотрел на id, сообщение msg и is_interrupted: true. Здесь меня интересует весь маршрут: какой признак сработал, сколько баллов принёс и что в итоге прервало запрос. Так мы ищем причину сбоя, а не просто учим журнал молчать.

Пять подозреваемых по делу о пропавшей корзине

Первый — Socket.IO с text/plain. Правило 920420 не приняло тип содержимого служебных HTTP-запросов. В стенде они приходили с Content-Type: text/plain. Речь о handshake или polling, а не о проверке кадров уже установленного WebSocket-соединения. Здесь я намеренно приторможу: если перепутать эти вещи, будешь объяснять сбой совсем на другом уровне.

Второй — корзине понадобился PUT. Запрос к /api/BasketItems/1 упёрся в правило 911100. Конфигурация разрешала GET, POST, HEAD и OPTIONS. PUT в этот клуб не взяли. Приложение просит обновить данные, политика отказывает — и покупатель остаётся с несбывшимися потребительскими планами.

Третий — профиль с названием whoami. Правило 932260 распознало в /rest/user/whoami слово, похожее на Unix-команду. Но здесь это название REST-эндпоинта. Никто не запускает команду. Проверка нашла знакомый признак, а контекст приложения в комплекте к нему не прилагался.

Четвёртый — поиск apple'--. Апостроф и два дефиса зацепили 942100 из LibInjection. В лабораторном сценарии это было тестовое значение поиска. Для SQLi-детектора оно выглядело подозрительно. Вот тут, хрюшка, начинается самое интересное: сделать так, чтобы поиск работал, легко. Сохранить при этом нужную проверку — отдельная работа.

Пятый — IP-адрес в Host. Правило 920350 добавляло 3 балла за числовой Host. Само по себе оно не дотягивало до блокировки в этом примере, но могло суммироваться с другими предупреждениями. Так что я бы не назначал одного виновника лишь потому, что его имя последним оказалось в поле зрения.

Чиним корзину. Весь забор не сносим

Автор собрал исключения в coraza-rules/exclusions.conf. Вместо глобального SecRuleRemoveById он использовал точечное исключение целей через ctl:ruleRemoveTargetById. Правило продолжало работать для остальных запросов. Границы исключения всё равно нужно проверять: слово «точечный» не выдаёт настройке автоматический сертификат безопасности.

С поиском первая попытка оказалась слишком широкой. Параметр q для /rest/products/search исключили из проверки 942100. Ложное срабатывание исчезло, но эта проверка больше не анализировала исключённый параметр. Мой любимый вид зелёного теста: перестали проверять — перестало ругаться. Только вот это ещё не починка.

Тогда автор добавил условие через chain: исключение действует, если значение не содержит заданных SQL-шаблонов, в частности union, select или or 1=1. Здесь поставлю большой редакторский пятачок: источник прямо называет это упрощённым учебным примером. Готовым правилом для production он от этого не становится.

Обфускация, кодирование и SQL-комментарии могут обойти такое регулярное выражение. В разборе отдельно упомянуты нормализация и трансформации t:urlDecodeUni, t:lowercase. Поэтому переносить это регулярное выражение в свою конфигурацию без разбора не советую. Придётся проверить и границы исключения, и то, какие запросы оно теперь пропускает.

Покупателя пустили. Теперь время незваных гостей

После изменений Playwright дошёл до кнопки оформления заказа. В журнале больше не было блокировок этого сценария по пяти рассмотренным правилам. Хорошо: функциональность стенда восстановили. Но я бы пока не откладывал клавиатуру и не наливал праздничный кофе.

Автор отдельно отправил тестовые запросы с сигнатурами SQLi, RCE, XSS, Path Traversal и запрещённым методом DELETE. Они продолжили получать 403. Это результат конкретных проверок на конкретном стенде, а не обещание заблокировать любую атаку после подобных изменений.

В стенде использовались Coraza с CRS 4.25.2 и Caddy. Для production автор добавляет версионирование исключений, ревью, мониторинг и автоматизированную регрессию; область исключения желательно сужать до метода, URL и параметра. Вот такой финал у Пигмена: корзина работает, тестовые атаки блокируются, а следующий инженер может разобраться в конфигурации без вызова духов из закрытых тикетов.