Сайт, принимающий любые формы и параметры, рано или поздно встречает гостя с консолью в руках. SQL-инъекция в адресной строке, скрипт в поле поиска, подозрительный файл в загрузках - всё это приезжает по обычному HTTPS, и веб-сервер обязан уметь отказывать опасным запросам сам. Стандартный инструмент здесь ModSecurity с открытым набором правил OWASP CRS. Загвоздка в том, что для nginx готового пакета нет: связку из библиотеки libmodsecurity, коннектора и набора правил собирают вручную. Ниже разобран путь от чистой Ubuntu до заблокированной инъекции с проверкой каждого шага.
Архитектура ModSecurity 3 с libmodsecurity и коннектором для Nginx
Вторая версия ModSecurity жила модулем к Apache и была привязана к нему по рукам и ногам. Третья версия архитектуру перевернула: движок проверки вынесен в отдельную библиотеку libmodsecurity, а веб-серверы общаются с ней тонкими коннекторами. Для nginx это динамический модуль ngx_http_modsecurity_module, который отвечает только за стыковку с сервером, вся логика фильтрации живёт в библиотеке. Такой расклад даёт общий движок для разных платформ и обновление библиотеки без пересборки веб-сервера.
Есть и цена: часть возможностей старой версии не переехала, поддержка скриптов Lua пока отсутствует. Осенью 2026 актуальная библиотека - v3.0.17, релиз конца сентября, коннектор для nginx - v1.0.4. Свежесть здесь не прихоть: в ветке 3.0.x постоянно правят серьёзные вещи, от обхода проверки тел ответов до падений при разборе XML, поэтому версию держат свежей.
Как движок ловит атаку. Правило SecRule описывает условие: переменную (ARGS, REQUEST_URI, REQUEST_HEADERS и другие), оператор сравнения (@rx для регулярных выражений, @eq для чисел, встроенные детекторы @detectSQLi и @detectXSS) и список действий при совпадении, среди которых id с номером правила, msg с человеческим описанием и block, который и останавливает запрос. Перед сравнением к значению применяют трансформации вроде t:urlDecode или t:lowercase, чтобы атаки с кодированием не проскакивали. Обработка идёт по фазам: сначала заголовки запроса, затем его тело, потом заголовки и тело ответа, в конце журналирование. Общий выключатель - директива SecRuleEngine со значениями On, Off и DetectionOnly: первый блокирует, второй отключает проверку, третий только записывает нарушения в журнал, не мешая запросу дойти до сайта.
Сборка libmodsecurity из исходников и подготовка зависимостей
Библиотека собирается классическим путём configure и make, набор зависимостей умеренный:
sudo apt update
sudo apt install -y git gcc g++ make build-essential \
autoconf automake libtool pkg-config libpcre2-dev \
libssl-dev zlib1g-dev libxml2-dev libyajl-dev \
liblmdb-dev libcurl4-openssl-dev
Дальше сама сборка:
cd /usr/local/src
git clone --depth 1 -b v3.0.17 https://github.com/owasp-modsecurity/ModSecurity
cd ModSecurity
git submodule init
git submodule update
./build.sh
./configure
make -j$(nproc)
sudo make install
sudo ldconfig
Несколько пояснений. Сабмодули обязательны: часть внутренних зависимостей движка, включая детектор libinjection, подтягивается из вложенных репозиториев. Скрипт build.sh проверяет окружение и готовит configure, он же первым скажет, какой библиотеки не хватает. Метка v3.0.17 это последний на момент осени 2026 релиз, её меняют по мере выхода новых версий. После установки библиотеку регистрируют в системе через ldconfig, иначе nginx не найдёт файлы при загрузке модуля.
Сборка на средней виртуальной машине занимает минут десять, на слабой вдвое дольше. Начинать лучше на тестовом сервере: для коннектора понадобится та же версия nginx, что стоит на боевой машине, и её исходники.
Сборка коннектора ModSecurity nginx и подключение модуля к веб серверу
Коннектор компилируется как динамический модуль, а nginx привередлив: модуль грузится, только если собран с той же версией и совместимой строкой конфигурирования. Сначала смотрят, что стоит на сервере:
nginx -V 2>&1 | tail -1
Вывод даёт версию и строку аргументов configure. Дальше качают исходники именно этой версии, а к прежним аргументам добавляют два новых, --with-compat и --add-dynamic-module с путём к коннектору:
cd /usr/local/src
git clone --depth 1 https://github.com/owasp-modsecurity/ModSecurity-nginx
curl -O https://nginx.org/download/nginx-1.30.5.tar.gz
tar xf nginx-1.30.5.tar.gz
cd nginx-1.30.5
./configure --with-compat \
--add-dynamic-module=../ModSecurity-nginx \
# сюда же прежние аргументы configure из вывода nginx -V
make modules
sudo cp objs/ngx_http_modsecurity_module.so /etc/nginx/modules/
Параметр --with-compat обязателен: без него модуль откажется загружаться из-за малейшего расхождения в аргументах сборки. Файл ngx_http_modsecurity_module.so после компиляции лежит в каталоге objs, его копируют к остальным модулям сервера.
Загрузка и включение в nginx.conf:
load_module modules/ngx_http_modsecurity_module.so;
http {
server {
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/main.conf;
}
}
Директива modsecurity on включает фильтрацию для блока server или location, modsecurity_rules_file указывает файл с правилами. Проверка синтаксиса и перезагрузка:
sudo nginx -t && sudo nginx -s reload
Базовый конфиг modsecurity.conf и переключение режима из DetectionOnly
Конфигурацию движка держат в каталоге /etc/nginx/modsec. Отправная точка - рекомендованный файл из комплекта библиотеки:
sudo mkdir -p /etc/nginx/modsec
sudo cp /usr/local/src/ModSecurity/modsecurity.conf-recommended \
/etc/nginx/modsec/modsecurity.conf
Ключевые строки после копирования:
SecRuleEngine DetectionOnly
SecRequestBodyAccess On
SecRequestBodyLimit 13107200
SecAuditEngine RelevantOnly
SecAuditLog /var/log/nginx/modsec_audit.log
Смысл по порядку. SecRuleEngine DetectionOnly значит, что движок ничего не блокирует, а только пишет журнал: именно в таком режиме начинают работу, чтобы неделя наблюдения показала, какие правила срабатывают на честных посетителях. SecRequestBodyAccess On включает разбор тел POST-запросов, без него форма входа и комментарии сайта остаются без защиты. SecRequestBodyLimit ограничивает размер разбираемого тела, базовое значение чуть больше двенадцати мегабайт. Журнал аудита в режиме RelevantOnly пишется только по значимым событиям, а формат в третьей версии настраивается, доступен и JSON для отправки в системы разбора журналов.
Файл main.conf связывает всё в одну цепочку:
# /etc/nginx/modsec/main.conf
Include /etc/nginx/modsec/modsecurity.conf
Include /usr/local/crs/crs-setup.conf
Include /usr/local/crs/rules.conf
Первая строка - настройка движка, вторая и третья подключают набор правил, о котором дальше.
Установка набора правил OWASP CRS и выбор уровня Paranoia
Базовый движок без правил молчит, реально ловит атаки набор OWASP CRS - сотни проверенных правил против известных классов атак. Актуальная осенью 2026 версия v4.30.0, релиз начала октября. Установка:
cd /usr/local
sudo git clone --depth 1 -b v4.30.0 \
https://github.com/coreruleset/coreruleset crs
cd crs
sudo cp crs-setup.conf.example crs-setup.conf
sudo cp rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example \
rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
sudo cp rules/REQUEST-999-EXCLUSION-RULES-AFTER-CRS.conf.example \
rules/REQUEST-999-EXCLUSION-RULES-AFTER-CRS.conf
Внутри набора всё разложено по классам атак: SQL-инъекции живут в правилах с номерами 942, XSS с номерами 941, удалённое выполнение команд под номерами 932, у каждого блока свой файл. Движком многих проверок служит libinjection, эвристический детектор, обученный на реальных примерах инъекций: правило 942100 ловит SQL по нему, 941100 - XSS. Точность у детектора высокая, а число ложных тревог невелико.
Строгость набора задаётся уровнем Paranoia в файле crs-setup.conf:
SecAction "id:900000,phase:1,pass,t:none,nolog,setvar:tx.paranoia_level=1"
Уровни отличаются так:
- первый ловит только явные атаки и почти не трогает честных посетителей, его ставят сайтам с обычными формами;
- второй добавляет проверки неочевидных признаков, ложные срабатывания уже случаются, но редки;
- третий проверяет каждый параметр и каждую куку, под подозрение попадают легальные данные вроде Base64 в полях;
- четвёртый включает весь арсенал и годится стендам и закрытым сервисам, на живом сайте он обычно не задерживается.
Начинают с первого уровня и поднимают шаг за шагом, сверяясь с журналами.
Свежая версия набора важна не меньше свежей библиотеки. В релизе v4.30.0, например, закрыт обход командных инъекций в 932-м блоке, где команды прятались в пути к файлу, и подтянуты проверки параметров кодировки. Добавлены детекты утилит Active Directory и синтаксиса шаблонизаторов Velocity и FreeMarker. Атаки эволюционируют, и набор правил живёт обновлениями, поэтому копию в /usr/local/crs периодически обновляют командой git pull с перезагрузкой nginx после проверки конфигурации.
Проверка блокировки SQL инъекций и XSS на тестовом запросе
Пока включён DetectionOnly, проверка безопасна и показательна. Классическая инъекция в адресной строке:
curl -i "https://example.com/?id=1%20union%20select%20password"
Скрипт в параметре поиска:
curl -i "https://example.com/?q=%3Cscript%3Ealert(1)%3C/script%3E"
Ответ сервера в режиме наблюдения останется обычным, поэтому результат смотрят в журналах. В error.log nginx появится запись движка с номером правила и описанием вида [id "942100"][msg "SQL Injection Attack Detected via libinjection"], в журнале аудита - полный разбор запроса, включая совпавший параметр. Удобная сводка по номерам:
sudo grep -o 'id "[0-9]*"' /var/log/nginx/error.log | sort | uniq -c
После недели уверенного DetectionOnly движок переводят в боевой режим правкой одной строки:
SecRuleEngine On
И повторяют те же запросы. Теперь curl получает ответ с кодом 403, до сайта запрос не доходит, а в журнале остаётся полный след блокировки. Дальше стоит прогнать и честные сценарии: вход по форме, сохранение статьи с обычным текстом, загрузка картинки, поиск по сайту. Всё должно работать как раньше.
Ложные срабатывания и работа с исключениями для конкретных приложений
Рано или поздно движок зацепит легальный запрос: визуальный редактор отправляет HTML и попадает под правила XSS, JSON-интерфейс приложения выглядит подозрительно для парсеров, экспорт файлов напоминает сканирование. Сначала смотрят номер правила в журнале, дальше выбирают лечение по масштабу проблемы.
Для популярных систем в комплекте набора есть готовые пакеты исключений: правила REQUEST-903.9002 закрывают WordPress, REQUEST-903.9001 - Drupal, есть готовый набор и для Nextcloud. Они аккуратно ослабляют конкретные правила там, где поведение приложения легально, и это первый путь.
Второй путь - точечное исключение для проблемного раздела сайта. Внутри блока location пишут:
location /editor/ {
modsecurity_rules '
SecRuleRemoveById 941100 942100
';
}
Правила перестают действовать только для адресов этого раздела, остальной сайт защищён как раньше. Третий путь, самый тонкий - файлы исключений REQUEST-900 и REQUEST-999: в них правят отдельные правила, например убирают конкретный параметр формы из проверки по номеру:
SecRuleUpdateTargetById 942100 "!ARGS:content"
Чего делать нельзя - вырубать правило целиком на весь сайт из-за одного ложного срабатывания. Каждое глобальное выключение открывает щель, которой воспользуются не только редактор.
Обслуживание связки несложное: правила обновляют через git pull в каталоге набора, библиотеку - пересборкой при выходе релиза, конфигурацию проверяют командой nginx -t перед каждой перезагрузкой. Журналы периодически просматривают на новые номера правил, особенно после обновления набора: свежие проверки видят атаки, которых старые не замечали, и иногда присматривают лишнее в легальном трафике.
ModSecurity с набором OWASP CRS не заменяет обновления софта и аккуратный код, но работает страховкой на случай, когда уязвимость уже обнаружена, а патча ещё нет. Связка собирается за вечер, неделю трудится в режиме наблюдения, затем включается на полную силу. SQL-инъекция, которая ещё вчера уехала бы в базу, теперь отскакивает от входной двери с кодом 403, и в журнале остаётся её полное описание для разбора.