ModSecurity с набором правил OWASP CRS включают за десять минут, а выключают через три дня — после того, как перестают сохраняться статьи в админке, ломается загрузка файлов и клиент не может оформить заказ, потому что в адресе оказалась кавычка. Вывод «WAF мешает работать» напрашивается сам, но он неверный: почти все эти блокировки лечатся тремя-четырьмя точечными исключениями, и всё дело в том, чтобы найти их правильно.

Не включайте блокировку сразу

Первая неделя — только наблюдение. В /etc/modsecurity/modsecurity.conf:

SecRuleEngine DetectionOnly

В этом режиме WAF пишет всё, что заблокировал бы, но ничего не блокирует. Неделя реального трафика — включая ваши собственные действия в админке, загрузку картинок и оформление заказа — даст список настоящих ложных срабатываний, а не гипотетических. Переключать на On имеет смысл только после того, как этот список разобран.

Как устроен CRS: не одно правило, а сумма

Ключ к пониманию всего дальнейшего. CRS почти никогда не блокирует запрос одним правилом. Каждое сработавшее правило добавляет запросу баллы аномальности, и блокировка происходит, когда сумма превысит порог. Поэтому в логе вы увидите не одну строку, а несколько, и последней будет правило с идентификатором 949110 — то самое, которое подводит итог.

Практический вывод: исключение надо делать для правила, которое набрало баллы, а не для 949110. Отключив итоговое правило, вы отключите весь набор целиком, и WAF останется только в виде строчки в конфиге.

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

Найти виноватое правило

Всё нужное есть в audit log (/var/log/modsec_audit.log) и в логе ошибок веб-сервера. Ищем по времени блокировки:

sudo grep -o 'id "[0-9]*"' /var/log/modsec_audit.log | sort | uniq -c | sort -rn | head

Это частотный список сработавших правил. Дальше по конкретному идентификатору смотрим, на чём именно оно сработало:

sudo grep -A5 'id "942100"' /var/log/modsec_audit.log | head -40

Нужны три вещи: идентификатор правила, имя параметра (ARGS:content, ARGS:comment) и путь запроса. Из них и строится исключение.

Обычные подозреваемые

Список повторяется от сайта к сайту:

  • 942100 — SQL-инъекция. Срабатывает на текстовых полях с длинным содержимым: тело статьи, описание товара, комментарий. Кавычки, скобки и слова вроде select в нормальном тексте выглядят для детектора подозрительно;
  • 941100 и 941xxx — XSS. Приходят вместе с визуальным редактором: HTML-теги в поле и есть его смысл работы;
  • 920420 — неразрешённый Content-Type. Ломает API и загрузку файлов: набор разрешённых типов по умолчанию узкий и application/json в старых версиях в него не входил;
  • 913100 — сканер по User-Agent. Ловит вместе со сканерами и легальные инструменты: мониторинг доступности, curl в ваших же скриптах;
  • 200002, 200004 — ошибки разбора тела запроса. Обычно означают не атаку, а превышенный лимит на размер тела, то есть загрузку крупного файла.

Три способа сделать исключение

По возрастанию грубости. Все они пишутся в свой файл (например, /etc/modsecurity/crs/REQUEST-900-EXCLUSION-RULES.conf), а не в файлы самого CRS: набор правил обновляется, и ваши правки при обновлении исчезнут.

Убрать один параметр из-под одного правила. Самый точный вариант, к нему и стремимся:

SecRuleUpdateTargetById 942100 "!ARGS:content"

Отключить правило только на одном пути. Годится, когда «шумит» конкретная страница — редактор, импорт, форма отзыва:

SecRule REQUEST_URI "@beginsWith /admin/post" \
    "id:1001,phase:1,pass,nolog,ctl:ruleRemoveById=942100"

Отключить правило целиком. Крайняя мера и почти всегда признак того, что причину не нашли:

SecRuleRemoveById 942100

Разница между первым и третьим — существенная. В первом случае поле «текст статьи» перестаёт проверяться на SQL-инъекции у одного правила; во втором и третьем перестаёт проверяться весь сайт. Разница в объёме работы — минут пять.

После правок — проверка конфигурации и мягкий перезапуск:

sudo apachectl configtest && sudo systemctl reload apache2
sudo nginx -t && sudo systemctl reload nginx

Порядок, который экономит неделю

  1. Неделя в DetectionOnly с обычной работой на сайте, включая админку и загрузки.
  2. Частотный список правил из audit log. Разбирать сверху вниз — первые три-четыре дадут девяносто процентов шума.
  3. По каждому: понять, какой параметр и на какой странице. Исключение делать по параметру, а не по правилу.
  4. Только теперь SecRuleEngine On.
  5. Раз в месяц заглядывать в блокировки: изменился сайт — появились новые ложные срабатывания.

Последний пункт и есть то, обо что всё обычно разбивается: audit log — это гигабайты текста, читать его вручную никто не станет. Смысл собранной панели в том, чтобы список сработавших правил, заблокированных запросов и активного набора был перед глазами, а не добывался из grep. Как это выглядит — на странице демо ниже.