WAF с ModSecurity: как мы защитили интернет-магазин от SQL-инъекций и веб-атак

Ситуация: магазин под постоянными атаками

Интернет-магазин «МагазинОнлайн» обратился к нам в itfresh.ru после серии инцидентов: за последний месяц сайт трижды пытались взломать через SQL-инъекции в форме поиска товаров, дважды — через XSS в отзывах. Одна из атак оказалась успешной — злоумышленник получил дамп таблицы с email-адресами 14 000 клиентов.

Инфраструктура магазина:

  • Фронт — Nginx 1.24 как reverse proxy, 2 backend-сервера с PHP 8.2 + Laravel
  • БД — MySQL 8.0 с 45 таблицами, 2.3 млн строк товаров
  • Трафик — 15 000 уникальных посетителей/день, пики до 40 000 в распродажи
  • Защита — отсутствует: ни WAF, ни rate limiting, ни IP-блокировки

Логи Nginx за последнюю неделю показали масштаб проблемы:

# Считаем подозрительные запросы за неделю
grep -cE "(union.*select|concat.*0x|sleep\(|benchmark\(|/etc/passwd|\.\./)" /var/log/nginx/access.log
# Результат: 47 832 подозрительных запроса

# Топ-10 IP по количеству подозрительных запросов
grep -E "(union.*select|concat.*0x|sleep\()" /var/log/nginx/access.log \
  | awk '{print $1}' | sort | uniq -c | sort -rn | head -10
#  12847 185.220.101.34
#   8932 45.148.10.74
#   6721 178.62.55.12
#   4210 91.132.147.88
#   ...

Задача: развернуть WAF, который блокирует атаки без ложных срабатываний на легитимном трафике магазина.

Установка ModSecurity для Nginx

ModSecurity — open-source WAF, работающий как модуль Nginx или Apache. Мы выбрали вариант с Nginx, так как он уже использовался как reverse proxy. Для Nginx нужен модуль libmodsecurity3 (ModSecurity v3):

# Устанавливаем зависимости (Ubuntu 22.04)
sudo apt update
sudo apt install -y build-essential git libcurl4-openssl-dev \
  libyajl-dev libgeoip-dev liblmdb-dev libfuzzy-dev \
  libxml2-dev libpcre3-dev libssl-dev pkg-config

# Собираем libmodsecurity3
git clone --depth 1 https://github.com/owasp-modsecurity/ModSecurity.git
cd ModSecurity
git submodule init && git submodule update
./build.sh
./configure --with-pcre2
make -j$(nproc)
sudo make install

# Собираем connector для Nginx
git clone --depth 1 https://github.com/owasp-modsecurity/ModSecurity-nginx.git

# Получаем исходники того же Nginx, что установлен
nginx -v  # nginx/1.24.0
wget http://nginx.org/download/nginx-1.24.0.tar.gz
tar xzf nginx-1.24.0.tar.gz
cd nginx-1.24.0

# Собираем только модуль (dynamic module)
./configure --with-compat --add-dynamic-module=../ModSecurity-nginx
make modules
sudo cp objs/ngx_http_modsecurity_module.so /etc/nginx/modules/

Подключаем модуль в конфиг Nginx:

# /etc/nginx/nginx.conf
load_module modules/ngx_http_modsecurity_module.so;

http {
    modsecurity on;
    modsecurity_rules_file /etc/nginx/modsecurity/modsecurity.conf;
    # ...
}

Базовая конфигурация ModSecurity:

# /etc/nginx/modsecurity/modsecurity.conf

# Начинаем в режиме DetectionOnly — только логируем, не блокируем
SecRuleEngine DetectionOnly

# Логирование
SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
SecAuditLogType Serial
SecAuditLog /var/log/modsecurity/audit.log

# Лимиты запросов
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
SecRequestBodyAccess On
SecResponseBodyAccess Off

# Временные файлы
SecTmpDir /tmp/modsecurity/tmp
SecDataDir /tmp/modsecurity/data

# Unicode mapping
SecUnicodeMapFile unicode.mapping 20127

# Подключаем OWASP CRS
Include /etc/nginx/modsecurity/crs/crs-setup.conf
Include /etc/nginx/modsecurity/crs/rules/*.conf

OWASP CRS: настройка набора правил

OWASP Core Rule Set (CRS) — основной набор правил для ModSecurity. Содержит более 200 правил для защиты от OWASP Top 10:

# Устанавливаем OWASP CRS
cd /etc/nginx/modsecurity/
git clone https://github.com/coreruleset/coreruleset.git crs
cd crs
cp crs-setup.conf.example crs-setup.conf

Ключевые настройки CRS для интернет-магазина:

# /etc/nginx/modsecurity/crs/crs-setup.conf

# Paranoia Level: 1 (минимум) — 4 (максимум)
# Для магазина начинаем с PL2 — хороший баланс
SecAction "id:900000,phase:1,nolog,pass,t:none,\
  setvar:tx.blocking_paranoia_level=2"

# Detection paranoia level — логирует правила до этого уровня
SecAction "id:900001,phase:1,nolog,pass,t:none,\
  setvar:tx.detection_paranoia_level=3"

# Anomaly scoring thresholds
# Запрос блокируется если набрал >= inbound_anomaly_score_threshold
SecAction "id:900110,phase:1,nolog,pass,t:none,\
  setvar:tx.inbound_anomaly_score_threshold=5,\
  setvar:tx.outbound_anomaly_score_threshold=4"

# Разрешённые HTTP-методы
SecAction "id:900200,phase:1,nolog,pass,t:none,\
  setvar:'tx.allowed_methods=GET HEAD POST OPTIONS PUT DELETE'"

# Разрешённые Content-Types
SecAction "id:900220,phase:1,nolog,pass,t:none,\
  setvar:'tx.allowed_request_content_type=|application/x-www-form-urlencoded| |multipart/form-data| |text/xml| |application/xml| |application/soap+xml| |application/json| |application/octet-stream|'"

# Максимальная длина аргумента (для магазина увеличиваем — длинные описания товаров)
SecAction "id:900320,phase:1,nolog,pass,t:none,\
  setvar:tx.max_num_args=512,\
  setvar:tx.arg_name_length=128,\
  setvar:tx.arg_length=8192,\
  setvar:tx.total_arg_length=64000"

CRS использует anomaly scoring: каждое правило добавляет баллы к общему счёту запроса. Если счёт превышает порог — запрос блокируется. Это намного надёжнее, чем блокировка по одному совпадению: легитимный запрос может случайно попасть под одно правило, но не наберёт 5 баллов.

Режим DetectionOnly и переход к блокировке

Мы никогда не включаем блокировку сразу — первые 7 дней WAF работает в режиме DetectionOnly, только логируя подозрительные запросы без их блокировки. Это позволяет найти ложные срабатывания до того, как они сломают функциональность сайта.

# Анализируем логи ModSecurity за 7 дней
# Считаем срабатывания по правилам
grep -oP 'id "\K[0-9]+' /var/log/modsecurity/audit.log \
  | sort | uniq -c | sort -rn | head -20

#  4521 941100  — SQL Injection Attack Detected via libinjection
#  3892 942100  — SQL Injection Attack Detected via libinjection
#  1247 949110  — Inbound Anomaly Score Exceeded
#   892 932100  — Remote Command Execution (содержит /etc/ в URL)
#   234 921110  — HTTP Response Splitting
#   187 941160  — XSS Filter via libinjection
#    89 920420  — Request content type is not allowed
#    45 913120  — Scanner detection (User-Agent)
#    12 933210  — PHP Injection Attack
#     8 920350  — Host header is IP address

Большинство срабатываний — реальные атаки (941100, 942100). Но нашли и ложные срабатывания: правило 932100 блокировало URL /catalog/etc-product (название категории). Создаём исключение:

# /etc/nginx/modsecurity/crs/rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf

# Исключение: URL /catalog/ содержит "etc" в названии товаров
SecRule REQUEST_URI "@beginsWith /catalog/" \
  "id:1001,phase:1,nolog,pass,\
  ctl:ruleRemoveById=932100"

# Исключение: API корзины отправляет JSON с HTML-разметкой описаний товаров
SecRule REQUEST_URI "@beginsWith /api/cart" \
  "id:1002,phase:1,nolog,pass,\
  ctl:ruleRemoveTargetById=941100;ARGS:description,\
  ctl:ruleRemoveTargetById=941160;ARGS:description"

# Исключение: форма отзывов — пользователи пишут символы, похожие на SQL
SecRule REQUEST_URI "@beginsWith /api/reviews" \
  "id:1003,phase:2,nolog,pass,\
  ctl:ruleRemoveTargetById=942100;ARGS:text,\
  ctl:ruleRemoveTargetById=942200;ARGS:text"

После 7 дней тюнинга, убедившись что ложных срабатываний больше нет, включаем блокировку:

# Переключаем с DetectionOnly на On
SecRuleEngine On

# Перезагружаем Nginx
sudo nginx -t && sudo systemctl reload nginx

Кастомные правила и IP-whitelisting

Помимо OWASP CRS, мы написали кастомные правила под специфику магазина:

# /etc/nginx/modsecurity/custom-rules.conf

# Блокировка сканеров уязвимостей по User-Agent
SecRule REQUEST_HEADERS:User-Agent "@rx (?i)(nikto|sqlmap|nmap|dirbuster|havij|acunetix|w3af)" \
  "id:2001,phase:1,deny,status:403,log,\
  msg:'Vulnerability scanner detected',\
  tag:'automation/scanner',severity:'CRITICAL'"

# Защита от brute-force на /admin
SecRule REQUEST_URI "@beginsWith /admin/login" \
  "id:2002,phase:1,pass,nolog,\
  initcol:ip=%{REMOTE_ADDR},\
  setvar:ip.auth_attempts=+1,\
  expirevar:ip.auth_attempts=300"

SecRule IP:AUTH_ATTEMPTS "@gt 5" \
  "id:2003,phase:1,deny,status:429,log,\
  msg:'Brute force on admin login: %{IP.auth_attempts} attempts in 5 min',\
  chain"
  SecRule REQUEST_URI "@beginsWith /admin/login"

# Блокировка доступа к конфигурационным файлам
SecRule REQUEST_URI "@rx (?i)\.(env|git|svn|htaccess|htpasswd|bak|old|swp|sql|config)$" \
  "id:2004,phase:1,deny,status:403,log,\
  msg:'Attempt to access sensitive file: %{REQUEST_URI}',\
  severity:'CRITICAL'"

# Rate limiting API — не более 60 запросов в минуту
SecRule REQUEST_URI "@beginsWith /api/" \
  "id:2005,phase:1,pass,nolog,\
  initcol:ip=%{REMOTE_ADDR},\
  setvar:ip.api_requests=+1,\
  expirevar:ip.api_requests=60"

SecRule IP:API_REQUESTS "@gt 60" \
  "id:2006,phase:1,deny,status:429,log,\
  msg:'API rate limit exceeded: %{IP.api_requests} req/min from %{REMOTE_ADDR}',\
  chain"
  SecRule REQUEST_URI "@beginsWith /api/"

IP-whitelisting для доверенных адресов — офис, мониторинг, платёжные системы:

# /etc/nginx/modsecurity/whitelist.conf

# Офисная сеть — полностью исключена из WAF
SecRule REMOTE_ADDR "@ipMatch 91.210.45.0/24" \
  "id:3001,phase:1,allow,nolog,\
  ctl:ruleEngine=Off"

# Платёжная система (webhooks) — исключены из проверки тела запроса
SecRule REMOTE_ADDR "@ipMatch 185.71.76.0/22,185.71.77.0/24" \
  "id:3002,phase:1,pass,nolog,\
  ctl:ruleRemoveById=941100-941999,\
  ctl:ruleRemoveById=942100-942999"

# Мониторинг (healthcheck)
SecRule REMOTE_ADDR "@ipMatch 10.0.0.0/8" \
  "id:3003,phase:1,allow,nolog,\
  ctl:ruleEngine=Off"

Интеграция с fail2ban

ModSecurity блокирует отдельные запросы, но не банит IP-адреса. Если атакующий отправляет 10 000 запросов, каждый будет проанализирован (нагрузка на CPU). Решение — интеграция с fail2ban: после N заблокированных запросов IP банится на уровне iptables.

# /etc/fail2ban/filter.d/modsecurity.conf
[Definition]
failregex = .*\[client <HOST>\].*ModSecurity:.*
            .*"transaction_id".*"remote_address":"<HOST>".*"intercepted".*
ignoreregex =

# /etc/fail2ban/jail.d/modsecurity.conf
[modsecurity]
enabled  = true
filter   = modsecurity
logpath  = /var/log/modsecurity/audit.log
maxretry = 10
findtime = 300
bantime  = 3600
action   = iptables-multiport[name=modsecurity, port="80,443", protocol=tcp]
           telegram[chat_id="-100XXXXXXXXX", token="BOTTOKEN"]

# Повторные нарушители — бан на сутки
[modsecurity-recidive]
enabled  = true
filter   = modsecurity
logpath  = /var/log/modsecurity/audit.log
maxretry = 30
findtime = 86400
bantime  = 604800
action   = iptables-multiport[name=modsec-recidive, port="80,443", protocol=tcp]

Настраиваем Telegram-уведомления для fail2ban:

# /etc/fail2ban/action.d/telegram.conf
[Definition]
actionban = /usr/local/bin/fail2ban-telegram.sh ban <name> <ip> <bantime>
actionunban = /usr/local/bin/fail2ban-telegram.sh unban <name> <ip>

# /usr/local/bin/fail2ban-telegram.sh
#!/bin/bash
ACTION=$1
JAIL=$2
IP=$3
BANTIME=$4
CHAT_ID="-100XXXXXXXXX"
TOKEN="BOTTOKEN"

if [ "$ACTION" = "ban" ]; then
    # Получаем GeoIP информацию
    COUNTRY=$(geoiplookup "$IP" 2>/dev/null | awk -F': ' '{print $2}')
    MSG="🚫 *WAF Ban*%0A"
    MSG+="IP: \`${IP}\`%0A"
    MSG+="Jail: ${JAIL}%0A"
    MSG+="Country: ${COUNTRY}%0A"
    MSG+="Ban time: ${BANTIME}s"
else
    MSG="✅ *WAF Unban*%0AIP: \`${IP}\`%0AJail: ${JAIL}"
fi

curl -s "https://api.telegram.org/bot${TOKEN}/sendMessage" \
  -d "chat_id=${CHAT_ID}&text=${MSG}&parse_mode=Markdown" >/dev/null

Результат: за первую неделю fail2ban забанил 847 уникальных IP-адресов. Нагрузка на ModSecurity снизилась на 60%, потому что повторные запросы от атакующих отсекаются на уровне iptables, не доходя до Nginx.

Сравнение с NAXSI и облачными WAF

Перед внедрением мы рассмотрели альтернативы:

КритерийModSecurity + CRSNAXSICloudflare WAFAWS WAF
СтоимостьБесплатноБесплатно$20-200/мес$5 + $1/млн req
Набор правилOWASP CRS (200+ правил)Whitelist-basedManaged rulesManaged + custom
Обновления правилРучные (git pull)РучныеАвтоматическиеАвтоматические
Ложные срабатыванияСредне (нужен тюнинг)Мало (whitelist)МалоСредне
Производительность-5-10% throughput-2-3%0% (CDN)0% (CDN)
Контроль данныхПолный (on-premise)ПолныйТрафик через CFТрафик через AWS
Сложность настройкиВысокаяСредняяНизкаяСредняя

Мы выбрали ModSecurity по трём причинам: клиент хранит платёжные данные и не готов пропускать трафик через сторонние CDN; OWASP CRS покрывает 95% атак из коробки; full control — можно писать любые кастомные правила. NAXSI отпал из-за whitelist-подхода: для магазина с сотнями форм и API endpoints это слишком трудоёмко.

Результаты и мониторинг

После месяца работы WAF в боевом режиме статистика:

МетрикаДо WAFПосле WAF
Заблокированных атак/день0 (нет защиты)2 800
SQL injection попыток/день~6 0000 дошло до БД
XSS попыток/день~1 2000 дошло до клиентов
Забаненных IP/неделю0~850
Ложных срабатываний/день2-3 (тюнятся)
Влияние на latency+8 мс (p95)

Для мониторинга эффективности WAF мы настроили дашборд, который парсит audit-лог и отправляет метрики в Prometheus:

# /etc/cron.d/modsecurity-metrics
*/5 * * * * root /opt/scripts/modsec-metrics.sh

# /opt/scripts/modsec-metrics.sh
#!/bin/bash
LOG="/var/log/modsecurity/audit.log"
METRICS_FILE="/var/lib/prometheus/node-exporter/modsec.prom"

BLOCKED=$(grep -c 'was denied' "$LOG" 2>/dev/null || echo 0)
SQLI=$(grep -c '94[12]1[0-9][0-9]' "$LOG" 2>/dev/null || echo 0)
XSS=$(grep -c '941[0-9][0-9][0-9]' "$LOG" 2>/dev/null || echo 0)
SCANNER=$(grep -c '913[0-9][0-9][0-9]' "$LOG" 2>/dev/null || echo 0)

cat > "$METRICS_FILE" <<EOF
# HELP modsec_blocked_total Total blocked requests
# TYPE modsec_blocked_total counter
modsec_blocked_total $BLOCKED
# HELP modsec_sqli_total SQL injection attempts
# TYPE modsec_sqli_total counter
modsec_sqli_total $SQLI
# HELP modsec_xss_total XSS attempts
# TYPE modsec_xss_total counter
modsec_xss_total $XSS
# HELP modsec_scanner_total Scanner detections
# TYPE modsec_scanner_total counter
modsec_scanner_total $SCANNER
EOF

Рекомендации для тех, кто внедряет WAF самостоятельно: всегда начинайте с DetectionOnly, минимум 5-7 дней; обязательно исключайте IP платёжных систем; настраивайте fail2ban — ModSecurity в одиночку не спасёт от DDoS; регулярно обновляйте CRS (git pull раз в месяц). Если нужна помощь с настройкой WAF под ваш проект — обращайтесь к нам в itfresh.ru.

Часто задаваемые вопросы

На практике — от 3 до 10 мс дополнительной задержки на запрос при Paranoia Level 2. Это незаметно для пользователей. Основная нагрузка — на CPU: при 1000 req/s ModSecurity потребляет около 15% одного ядра. Для высоконагруженных проектов стоит рассмотреть NAXSI (легче) или вынести WAF на отдельный сервер.
Рекомендуем раз в месяц делать git pull и проверять changelog. Критические обновления (новые CVE) выходят быстрее — подпишитесь на GitHub Releases. После обновления обязательно переключайте SecRuleEngine в DetectionOnly на 1-2 дня, чтобы проверить совместимость с вашим трафиком.
Да, для Apache ModSecurity v2 устанавливается проще — через apt install libapache2-mod-security2. Правила OWASP CRS одинаковые для обоих серверов. Разница в производительности минимальна. Если у вас уже Apache — нет смысла мигрировать на Nginx ради WAF.
Нет. WAF — это дополнительный слой защиты, а не замена. Prepared statements для SQL-запросов, экранирование вывода для XSS, валидация ввода — это по-прежнему обязанность приложения. WAF ловит то, что пропустил разработчик, и блокирует атаки на известные уязвимости фреймворков.
Используйте OWASP ZAP или Nikto — они генерируют безопасные тестовые атаки. Также CRS содержит тестовый набор ftw (Framework for Testing WAFs): go-ftw запускает тысячи тестовых запросов и проверяет, что каждое правило срабатывает корректно. Для ручного теста достаточно curl: curl 'https://site.ru/?id=1 UNION SELECT 1,2,3' — должен вернуть 403.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#WAF#ModSecurity#OWASP CRS#SQL injection#XSS#fail2ban#Nginx#rate limiting
Комментарии 0

Оставить комментарий

загрузка...