Представьте, что вы включили набор правил OWASP CRS перед магазином, посмотрели на график блокировок и остались довольны: цифра растёт, значит, система работает. А через пару дней менеджер пересылает письмо: покупательница из Нижнего Новгорода третий раз пытается указать адрес доставки, и каждый раз получает страницу с кодом 403. Другой покупатель дошёл до оплаты, но после возврата с платёжной страницы увидел пустой экран. График блокировок при этом выглядит прекрасно, ведь в него одинаково попадают и ботнет, и живые люди с чужими фамилиями и странными комментариями для курьера.
Эта статья про то, как не оказаться в такой ситуации: как ввести WAF так, чтобы отличать атаку от обычного оформления покупки, и как пройти путь от режима наблюдения до осторожной блокировки, не ломая ни адрес, ни оплату, ни уведомление от платёжного провайдера.
Обычный заказ, который выглядит подозрительно
Начнём не с правил, а с запроса. Вот условная форма адреса доставки, которую отправляет добросовестный покупатель. Все данные в примере вымышленные.
POST /checkout/address
first_name=Мария
last_name=Д'Артаньян
city=Нижний Новгород
street=ул. Рождественская, д. 5, корп. 2; кв. 14
comment=Позвоните за час. Если не отвечу — оставьте у соседей (кв. 15), выберите любое удобное время
Человек прочитает это без труда. Теперь посмотрим глазами общего правила, которое ищет признаки внедрения SQL или команд. Апостроф в фамилии, точка с запятой в адресе, круглые скобки, слова «выберите» и «оставьте» (а в английской версии формы — select, union, drop в самом естественном контексте), длинная строка с запятыми. Каждый из этих элементов по отдельности безобиден, но у некоторых правил есть шаблоны, которые на такие символы реагируют.
Причина проста, и её прямо описывает документация проекта: CRS распознаёт общие шаблоны атак и изначально не знает особенностей конкретного приложения. Набор правил не видел вашу форму. Он не знает, что поле «комментарий» — это свободный текст, в котором люди пишут что угодно, что поле «улица» допускает точку с запятой, а параметр с возвратом из платёжной системы выглядит как длинная абракадабра, потому что это подписанный токен. Он знает только, как выглядят типичные атаки, и сравнивает с ними всё подряд.
Документация называет следствие: легитимный запрос может совпасть с правилом, и это ложное срабатывание (false positive). Для магазина это не абстрактная неприятность. Ложное срабатывание на странице блога стоит одного не прочитанного абзаца. На странице оформления заказа оно стоит заказа.
Где в магазине живут «странные» значения
Полезно заранее пройтись по пути покупателя и отметить места, где поля свободные или технические. Это ваша карта риска ложных срабатываний:
- Имя и фамилия. Апострофы, дефисы, точки, составные фамилии, иногда латиница вперемешку с кириллицей.
- Адрес. Знаки препинания, скобки, дробь («12/3»), номера корпусов, иногда кавычки в названии садоводческого товарищества.
- Комментарий к заказу. Самое непредсказуемое поле: полноценный текст, который человек пишет в спешке.
- Промокод. Короткая строка, но люди вставляют её вместе с пробелами и кавычками при копировании.
- Возврат с платёжной страницы. Длинные параметры: идентификатор заказа, статус, подпись, иногда закодированные в base64 данные.
- Уведомление от провайдера. Запрос не от человека, чаще всего JSON или форма с подписью, без привычных заголовков браузера.
Эта карта пригодится позже: именно по ней мы будем решать, где исключение уместно, а где нет.
Сначала смотреть, потом решать
Главная ошибка при вводе WAF — включить его сразу в режиме блокировки, потому что «так безопаснее». Безопаснее для чего? Для сайта, который никто не может использовать? Разумный порядок обратный: движок работает в режиме обнаружения, правила оценивают каждый запрос и пишут в журнал, но ничего не отклоняют. В ModSecurity для этого есть режим только наблюдения. Покупатели его не замечают, а вы получаете то, чего нельзя получить из документации: картину реального трафика вашего магазина.
Сколько держать такой режим, зависит от трафика, а не от календаря. Неделя на магазине с сотней заказов в день даст больше материала, чем месяц на магазине с десятком. Ориентир не в днях, а в охвате: за период наблюдения через сайт должны пройти все значимые сценарии. Обычный заказ, заказ с промокодом, регистрация, вход, заказ с оплатой каждым способом, возврат с оплаты, отмена, личный кабинет, возврат товара. Если какой-то сценарий за период не случился (например, оплата по одному из способов), его нужно провести вручную.
Что делать с журналом, чтобы он сам не стал проблемой
Журнал WAF — это не безобидный технический файл. Когда правило срабатывает, в запись может попасть содержимое запроса: имя, адрес, телефон, значение комментария. А если запрос содержит секрет, например токен сессии, ключ API или, не дай бог, номер карты, он окажется в записи дословно. Поэтому ещё до первого срабатывания нужно принять несколько решений.
- Номера карт и коды не должны попадать на ваш сервер вообще. Если платёжная форма размещена на стороне провайдера, а ваш магазин получает только статус и идентификатор платежа, то чувствительные платёжные данные не проходят через WAF. Это лучший способ защитить их в журнале: не иметь их. Если же ваша конструкция предполагает приём реквизитов на вашей стороне, это отдельный серьёзный разговор о требованиях индустрии, и подробный журнал тела запроса для таких путей недопустим.
- Для путей оформления и оплаты сократите журналирование тела. Для разбора ложного срабатывания чаще достаточно знать: какое правило, какой параметр, какой путь, какой фрагмент совпал. Полный дамп тела запроса обычно не нужен.
- Скрывайте значения чувствительных полей. Пароли, токены, заголовки авторизации, куки сессии, подписи уведомлений: их стоит маскировать на уровне настроек журнала, а не надеяться, что «никто не заглянет».
- Ограничьте доступ и срок хранения. Журналы WAF читают те, кто разбирает инциденты. Остальным они не нужны. Срок хранения должен быть коротким и осознанным: настолько, насколько нужно для разбора и расследования.
- Для тестов используйте условные данные. Когда будете воспроизводить проблему, заводите тестового покупателя с заведомо вымышленными адресом и именем, а не копируйте запись реального человека. Платёжные проверки делайте на тестовой среде провайдера, с тестовыми реквизитами, которые он сам даёт.
Есть и вполне практическая сторона. Аудитный журнал занимает место и нагружает диск. На небольшом VPS это ощутимо: если полный журнал пишется на каждый запрос, диск может заполниться неожиданно быстро, а запись будет конкурировать с самим магазином за ввод-вывод. Журналировать разумно только срабатывания, а не весь трафик, и следить за ротацией. Объём диска и скорость накопителя здесь не абстракция: именно на них упирается привычка «писать всё на всякий случай».
Разбор одного срабатывания по косточкам
Теперь представим, что за первые дни наблюдения в журнале появилась запись о нашей покупательнице из примера выше. Ниже условный фрагмент, составленный для объяснения, а не снятый с реальной системы: идентификатор правила здесь заменён заглушкой, а формат упрощён.
путь: /checkout/address
метод: POST
параметр: ARGS:comment
правило: 9XXXXX (категория: признаки внедрения SQL)
совпавший фрагмент: «...выберите любое удобное время...»
решение движка: только запись, запрос пропущен
Вот что стоит выяснить, прежде чем что-либо менять.
Первое: что именно совпало и в каком параметре. Правило сработало не на «запрос вообще», а на конкретное значение конкретного аргумента. Здесь это поле комментария. Это уже половина решения: проблема локализована.
Второе: нормальное ли это значение для этого поля. Да: комментарий курьеру — свободный текст, в нём может быть любая фраза. Если бы то же правило сработало на поле «номер телефона», где допустимы только цифры, то это был бы тревожный сигнал, а не ложное срабатывание. Один и тот же фрагмент в разных полях означает разное.
Третье: сколько таких записей и откуда. Если срабатывание единичное и источник — один адрес, который в тот же час пробует десятки разных параметров, это, скорее всего, сканер. Если срабатывание повторяется у разных покупателей, у каждого из которых после этого запроса следует успешное оформление заказа, перед нами закономерность, то есть ложное срабатывание. Сопоставление с журналом самого магазина здесь бесценно: заказ создан — значит, запрос был честным.
Четвёртое: что произойдёт после включения блокировки. В нашем примере правило должно было бы отклонить запрос, и покупательница получила бы 403 на шаге «адрес». Это нужно записать как конкретный риск, а не как общее чувство тревоги.
Обратите внимание, что на этом этапе мы не пытаемся «починить правило». Мы лишь убеждаемся, что понимаем, почему оно сработало, и можем объяснить это другому человеку одной фразой: «Правило против SQL-внедрения реагирует на слова в свободном комментарии при оформлении заказа». Если такую фразу составить не получается, значит, разбор не закончен.
Узкое исключение вместо выключения целого класса проверок
Теперь соблазн. В конфигурации легко найти способ отключить правило по номеру целиком, или даже целую категорию правил, и проблема исчезнет. Только вместе с ней исчезнет и защита от внедрения SQL на всём сайте, включая страницы, где она нужна: поиск, фильтры каталога, личный кабинет. Это обмен маленькой проблемы на большую дыру.
Документация CRS говорит об этом прямо: ложные срабатывания устраняют точечными исключениями правил (rule exclusions), а не отключением защиты. Точечность измеряется по трём осям: какое правило, какой параметр, какой путь. Чем уже исключение по всем трём, тем меньше защиты вы теряете.
| Способ | Что отключается | Что теряется |
|---|---|---|
| Отключить правило глобально | Это правило для всех параметров и всех страниц | Проверка поиска, фильтров, личного кабинета и всего остального по этому шаблону |
| Исключить правило для пути | Правило на странице адреса, для всех её полей | Проверка остальных полей формы: имя, город, телефон |
| Исключить правило для одного параметра на одном пути | Правило только для поля комментария на странице адреса | Проверка именно этого одного поля по этому одному шаблону |
Третий вариант почти всегда лучший. Он описывает ваше знание о приложении: «в этом поле на этой странице допускается свободный текст». Схематично такое исключение в синтаксисе ModSecurity выглядит примерно так (номер правила и путь здесь условные, а точная форма зависит от версии движка и способа подключения, поэтому сверяйтесь с документацией той версии, что у вас стоит):
SecRule REQUEST_URI "@beginsWith /checkout/address" "id:1000001,phase:1,pass,nolog,ctl:ruleRemoveTargetById=9XXXXX;ARGS:comment"
Читается оно так: «если запрос идёт на путь адреса, то правило 9XXXXX не должно проверять аргумент comment». Всё остальное по-прежнему проверяется этим правилом. Остальные правила по-прежнему проверяют и комментарий. Свои исключения лучше хранить в отдельном файле, с номерами из вашего собственного диапазона и с комментарием рядом: когда добавлено, по какому поводу, кто решил. Через полгода вы сами не вспомните, зачем здесь это исключение, и файл без пояснений превратится в кладбище загадок.
Про уровень паранойи
У CRS есть понятие paranoia level — уровень строгости. Документация отмечает, что более высокий уровень повышает вероятность ложных срабатываний. Логика понятна: высокий уровень включает проверки более подозрительных, но менее специфичных шаблонов. Для магазина отсюда следует практическое правило: не поднимайте уровень, пока не разобраны срабатывания на текущем. Каждая ступень вверх добавляет новый поток записей, который придётся разбирать по тому же алгоритму, и каждая из них умножает число нестандартных значений в форме адреса, которые вы будете встречать. Начинайте с базового уровня, доводите до тишины на нормальном трафике, и только потом решайте, нужна ли вам строже защита и готовы ли вы платить за неё временем разбора.
Эта дисциплина касается и ресурсов сервера. Чем больше включено правил, тем больше работы у процессора на каждый запрос, поскольку каждое значение каждого параметра проверяется по многим шаблонам. Для небольшого магазина на скромной конфигурации (например, 1–2 vCPU и 2–4 ГБ памяти, как в младших тарифах VPS MAATRIX на NVMe) разумно сначала замерить время ответа с WAF и без него на ваших реальных страницах, а не полагаться на общие представления. Ресурсы тарифа — это отправная точка для измерений, а не обещание, сколько запросов вы выдержите: это покажут только ваши собственные замеры. Если WAF стоит на том же сервере, что и сам магазин и база данных, он делит с ними процессор и память; когда замеры покажут нехватку, перейти на тариф с большим числом ядер проще, чем отказываться от защиты.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Оплата — это три разных запроса, а не один
Когда говорят «оплата не проходит», обычно имеют в виду один сценарий. На деле в нём как минимум три разных участника и три разных типа запросов. WAF смотрит на каждый из них по-своему, и исключения для них должны быть разными.
Запрос первый: покупатель уходит платить
Магазин создаёт заказ и отправляет покупателя на платёжную страницу провайдера. Это либо переадресация с параметрами, либо запрос вашего сервера к API провайдера. В первом случае основная часть работы проходит уже на стороне провайдера и ваш WAF её не видит. Во втором запрос идёт не через WAF, а из вашего приложения наружу, и входящие правила его не касаются. Для входящей части вопрос только в том, пройдёт ли обычное нажатие кнопки «Оплатить» с формы заказа. Обычно это самый безобидный запрос: короткие поля, скрытый токен формы. Проблемы возможны разве что на длинных скрытых значениях.
Запрос второй: уведомление провайдера
Это самый коварный запрос. После оплаты провайдер сам, без участия браузера, отправляет на адрес вашего магазина уведомление о статусе платежа. Он не похож на запрос человека: нет куки, нет привычных заголовков браузера, тело часто в JSON или в форме с подписью, значения содержат длинные идентификаторы, иногда вложенные структуры и закодированные строки. Для правила, написанного против вредоносных запросов, такая структура выглядит необычно, и ложные срабатывания здесь вполне вероятны.
При этом цена ошибки особенная. Если уведомление заблокировано, платёж на стороне провайдера прошёл, деньги списаны, а магазин об этом не знает. Заказ остаётся неоплаченным, покупатель недоумевает, поддержка получает разгневанное письмо. Провайдеры обычно повторяют уведомления несколько раз, но рассчитывать, что они дождутся, пока вы разберётесь, не стоит.
Что можно сделать здесь осмысленно, не снижая безопасность вслепую:
- Выделить отдельный путь для уведомлений, и только на нём применять особые исключения. Не смешивайте его с путями для покупателей.
- Не отключать WAF на этом пути целиком. Исключайте конкретные правила для конкретных параметров, которые вы видели в журнале при тестовых платежах.
- Проверять подлинность уведомления на уровне приложения, а не WAF. Если провайдер подписывает уведомления, проверка подписи — задача вашего кода. WAF не заменит её, а её наличие делает аккуратные исключения на этом пути гораздо менее опасными: подделать уведомление без ключа нельзя.
- Если провайдер публикует адреса, с которых шлёт уведомления, можно дополнительно ограничить этот путь по адресам. Но сверяйтесь с актуальной документацией провайдера и не считайте список вечным: он может меняться.
- Проверять уведомление на тестовом контуре провайдера с тестовыми данными, до рабочих платежей. В журнале WAF при этом будет видно, какие правила сработали.
Запрос третий: покупатель возвращается
После оплаты браузер покупателя возвращается на страницу магазина «Спасибо за заказ». В адресе часто приходят параметры: номер заказа, статус, иногда подпись или закодированная строка. Это снова другой запрос: GET от браузера, но со странной на вид, длинной строкой параметров. Правила, анализирующие аргументы строки запроса, могут принять base64-подобное значение за обфусцированную нагрузку.
Особенность этого запроса в том, что приложению нельзя на него полагаться: пользователь может не вернуться вовсе (закрыл вкладку), может вернуться дважды, может подменить параметры в адресной строке. Истинным сигналом об оплате служит уведомление, а страница возврата лишь показывает человеку результат. Поэтому если страница возврата попала под ложное срабатывание, ущерб, как правило, меньше: заказ всё равно оплачен и учтён, страдает только впечатление покупателя. Но впечатление тоже стоит денег: человек, увидевший 403 после платежа, пойдёт проверять банк и писать в поддержку.
Сведём всё вместе, чтобы увидеть различия:
| Запрос | Кто отправляет | Что необычного | Цена блокировки |
|---|---|---|---|
| Отправка формы заказа | Браузер покупателя | Свободный текст в адресе и комментарии | Заказ не оформлен |
| Уведомление об оплате | Сервер провайдера | Нет браузерных заголовков, JSON, подпись, длинные идентификаторы | Деньги списаны, заказ не отмечен оплаченным |
| Возврат на страницу магазина | Браузер покупателя | Длинные закодированные параметры в адресе | Пустая страница после оплаты, звонки в поддержку |
Из таблицы видно, почему единое исключение «для оплаты» — плохая идея. Три запроса требуют трёх разных решений, и каждое лучше формулировать как отдельное узкое исключение со своим путём и своим параметром. Заодно у вас остаётся возможность оставить для каждого пути свой уровень контроля: например, строгие правила на форме заказа и более осторожные на пути уведомлений, который защищён подписью.
Как пройти репетицию до включения блокировки
Прежде чем переводить хоть одно правило из наблюдения в блокировку, проведите сквозную проверку покупки, пока WAF всё ещё только записывает. Это дешёвый и очень информативный этап.
- Оформите тестовый заказ с вымышленным человеком, у которого фамилия с апострофом, адрес с точкой с запятой и скобками и комментарий из длинной свободной фразы.
- Оплатите его в тестовом режиме провайдера, с тестовыми реквизитами, которые выдаёт сам провайдер. Реальные карты для проверок не используйте и не вставляйте их ни в какие поля.
- Дождитесь уведомления и убедитесь, что заказ в магазине получил статус «оплачен».
- Вернитесь на страницу «Спасибо» тем же путём, которым вернулся бы покупатель.
- Откройте журнал WAF и выпишите все срабатывания, относящиеся к этим запросам: путь, параметр, правило.
- Для каждого срабатывания решите по алгоритму из раздела о разборе: ложное оно или нет, и если ложное, какое самое узкое исключение его устранит.
Повторите это для каждого способа оплаты и для сценариев, которые нетипичны, но возможны: отмена на платёжной странице, неудачная оплата, повторное уведомление. После добавления исключений прогоните сценарий ещё раз: журнал должен стать тихим на нормальном пути. Эти же проверки стоит повторять после каждого обновления набора правил и после каждого серьёзного изменения магазина: новая версия набора правил или новая форма легко приносят новые срабатывания.
Включение блокировки: по частям и без прощания с наблюдением
Когда нормальный трафик перестал давать срабатывания, а список исключений аккуратно задокументирован, наступает момент блокировки. Но и тут не нужно щёлкать один выключатель на всём сайте.
Безопасный порядок выглядит как лестница. Сначала блокировка включается для той части трафика, где ложных срабатываний не было и где вы лучше всего понимаете запросы, например, для служебных путей, которые покупателям не нужны вовсе: админ-панель, файлы конфигурации, типичные пути сканеров. Здесь любой запрос подозрителен по определению, и блокировка почти не рискует задеть людей. Затем наступает очередь публичных страниц каталога, и только после этого — страниц оформления заказа и платёжных путей, самых чувствительных. Между шагами стоит выдерживать паузу и смотреть в журнал: за это время вы должны успеть увидеть реальных покупателей, проходящих через новые правила.
Практичный способ контролировать риск — не только читать журнал блокировок, но и считать показатель, который действительно важен бизнесу: долю заказов, дошедших от адреса до оплаты. Если после очередного шага она заметно упала, это сигнал откатить шаг и разобраться, сколько бы красивых блокировок ни показывала панель. Для отката должна быть заранее известная кнопка: возможность вернуть конкретное правило или путь обратно в режим наблюдения за минуту, без правки десятка файлов.
И, наконец, наблюдение не заканчивается. Магазин меняется: появляется новый способ доставки, новое поле в форме, новый провайдер платежей, обновляется движок и набор правил. Каждое такое изменение способно породить новые ложные срабатывания, и лучше узнать о них из собственного журнала, чем от покупателя. Удобная привычка: после любого релиза просматривать срабатывания по путям оформления заказа за первые часы, пока трафик ещё невелик.
Что WAF не делает
Есть соблазнительная мысль: раз перед магазином стоит WAF с известным набором правил, то уязвимости в коде можно не торопиться исправлять. Это заблуждение, и оно опасно. Набор правил распознаёт типичные шаблоны, но не знает логики вашего приложения: он не заметит, что покупатель меняет номер заказа в адресе и открывает чужой, что скидка применяется дважды или что в личном кабинете не проверяются права доступа. Атака, которая выглядит как обычный запрос, пройдёт через него так же спокойно, как обычная покупка. WAF — это дополнительный слой, снижающий шум и закрывающий часть очевидных попыток, но не замена исправлению уязвимостей. Подробнее об этом заблуждении рассказано в материале «Миф: WAF закрывает дыры в коде».
Из этого следует и практический вывод для настройки: исключения, которые вы добавляете ради оплаты, делают правила чуть слабее в конкретных местах. Поэтому именно в этих местах код приложения должен сам проверять входные данные: длину, допустимые символы, подпись уведомления, принадлежность заказа текущему покупателю. Чем честнее приложение, тем безопаснее вам делать узкие исключения.
Для самой установки удобно иметь собственный сервер, где вы сами решаете, как подключить WAF перед магазином и как хранить журналы. На VPS MAATRIX доступен root-доступ, диски NVMe и процессоры AMD EPYC, а локации в публичном предложении указаны разные (UK, US, RU, FR). Ресурсы тарифа не равны обещанию ёмкости: сколько запросов выдержит ваша связка веб-сервера, WAF и базы данных, покажут только собственные замеры. Резервные копии и конкретные образы нужно проверять в форме заказа и настраивать самостоятельно, а не считать входящими по умолчанию.
Две записи в журнале рядом
Вернёмся к покупательнице из начала статьи. Спустя несколько недель после аккуратного ввода правил её заказ выглядит так: форма адреса с апострофом в фамилии, точкой с запятой в улице и фразой про соседей в комментарии прошла без отказа. Оплата в банке завершилась, уведомление от провайдера дошло, подпись проверена приложением, заказ получил статус оплаченного. Покупательница вернулась на страницу «Спасибо», увидела номер заказа и закрыла вкладку, так и не узнав, что между ней и магазином стоял WAF.
А в журнале в тот же час, в нескольких строках от её заказа, лежит другая запись. Запрос к странице поиска с параметром, в котором дважды повторяется конструкция внедрения SQL, без единого признака человеческого поведения: источник уже пробовал десяток разных путей, ни один из которых магазину не принадлежит. Правило, которое в наблюдении только фиксировало такие вещи, теперь отвечает на него кодом 403, и в записи видно, что запрос не дошёл до приложения.
Эти две записи, лежащие рядом, и есть настоящий результат настройки: не большое число блокировок и не тишина в журнале, а возможность показать пальцем на обычную покупку, которая прошла, и на нежелательный запрос, который не прошёл, и объяснить, по какому правилу и почему так вышло.