Модульный файрвол для PHP — это не про то, чтобы «заблокировать плохое». Это про то, чтобы на каждый запрос иметь короткий ответ: пропускать или нет, по какой причине, с каким счётом и какие правила сработали. PHP Shield 2.0.0 отвечает ровно так: один вызов Shield::inspect() возвращает массив решения с полями blocked, status, reason, detail, score и findings, а порядок проверок зафиксирован в коде — от дешёвых локальных до сетевых. Проект сменил имя (был SecurityMiddleware), получил репутацию IP через AbuseIPDB, блокировки по тяжести находки и панель. Ниже — как устроен порядок решений, что показали мои прогоны на PHP 8.5 и какие ловушки тут важнее самих правил.

php shield cover

TL;DR: что это и как проверить за десять минут

  1. Что это. Компонент для PHP 8.1–8.5: локальные правила (SQLi, XSS, инъекция команд, обход каталогов, подключение файлов, подозрительный User-Agent), репутация IP, лимиты частоты, блокировки и журнал инцидентов.
  2. Точка входа. Shield::inspect() → массив решения; isRequestAllowed() — булев фасад для тех, кому нужен только ответ «да/нет».
  3. Порядок фиксирован. Мастер-выключатель → доверенный адрес → активная блокировка → счётчик неудач → лимиты → репутация → проверка полей → проверка заголовков.
  4. По умолчанию безопасно. mode = monitor (только журнал), trusted_ips — только loopback, срок блокировки за лимит — 300 секунд.
  5. Проверка. Тесты проекта гоняются без базы: createWithComponents() не требует MySQL.
php -v                          # 8.1–8.5
composer install                # или composer require paulmann/php-shield
php vendor/bin/phpunit          # у меня: 141 тест, 333 проверки, OK за 4,6 с

Проблема, которую это решает

Правила безопасности обычно живут в трёх несвязанных местах: регулярки в контроллере, лимиты в конфиге веб-сервера и репутация «где-то во внешнем сервисе». Из-за этого на вопрос «почему этого клиента заблокировало» никто не отвечает: в журнале веб-сервера 403 без причины, в приложении ничего, а лимит частоты вообще считается другой подсистемой.

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

Третья проблема — доверие к заголовкам. X-Forwarded-For присылает клиент, а не прокси, если прокси не переписал его. Пока цепочка читается слева, клиент может подставить адрес из белого списка и пройти мимо всей защиты. Это не гипотетическая ошибка: в 2.0.0 она записана в списке исправленного.

Что такое PHP Shield и из чего состоит

Свойство Значение
Пакет paulmann/php-shield (был SecurityMiddleware)
Версия в замерах 2.0.0, релиз 07.11.2026
Требования PHP 8.1–8.5, ext-json, ext-mbstring
Необязательно ext-mysqli (MySQL), ext-redis (лимитер), ext-curl (репутация)
Код в src/ 44 файла, 3 911 строк
Тесты 19 файлов, 141 тест, 333 проверки
Правил обнаружения 6, каждый со своим диапазоном счёта
Лицензия MIT
flowchart TB
  REQ["HTTP-запрос"] --> EN{"включён?"}
  EN -- нет --> OK["пропустить"]
  EN -- да --> TR{"адрес в trusted_ips?"}
  TR -- да --> OK
  TR -- нет --> BL{"адрес заблокирован?"}
  BL -- да --> D403["403 ip_blocked"]
  BL -- нет --> FA{"счётчик неудач на пределе?"}
  FA -- да --> D403B["403 too_many_failures"]
  FA -- нет --> RL{"лимит частоты превышен?"}
  RL -- да --> D429["429 rate_limited"]
  RL -- нет --> REP["репутация IP: кэш, провайдер, предохранитель"]
  REP --> SC{"оценка abuse"}
  SC -- высоко --> D403C["403 ip_reputation"]
  SC -- средне --> J1["только журнал"]
  SC -- нет данных --> PAY
  J1 --> PAY
  PAY["проверка полей: 6 правил по GET, POST, REQUEST_URI"] --> HDR["проверка заголовков"]
  HDR --> DEC["решение: blocked, status, reason, detail, score, findings"]

Каталоги отражают слои: Middleware (решение), Detection (правила), ThreatIntel (репутация), RateLimiting (лимиты), Storage (блокировки и счётчики), Ip (определение адреса клиента), Support (конфигурация, заголовки), Admin (панель), Exception.

Проще говоря, это сборка из независимых частей: хранилище блокировок, лимитер и конфигурация. Что не подключено — то и не работает, но и не мешает.

Возможности

Обнаружение в полях и заголовках

Правило Категория Счёт Что ловит
sql_injection инъекция 25–80 UNION SELECT, составные запросы, SLEEP, тавтологии
xss XSS 40–90 теги script/iframe/object, обработчики событий, javascript:
command_injection инъекция команд 60–85 семейство system(), конвейеры в команды, обратные кавычки
path_traversal обход каталогов 60–95 повторные ../, /etc/passwd, .ssh, /proc/self
file_inclusion включение файлов 60–85 удалённые включения, обёртки php://
suspicious_header заголовки 60–70 User-Agent сканеров: sqlmap, nikto, nmap и подобные

Проверяются и «сырые» значения, и они же после URL-декодирования, плюс отдельно REQUEST_URI — поэтому %3Cscript%3E не проскочит. По одному правилу на поле фиксируется одна находка: дважды закодированная строка не накрутит счёт.

Репутация, лимиты и блокировки

Что Как устроено
Репутация IP AbuseIpDbProvider с конструкторами com() и ru(), проверка с резервным провайдером, общий кэш, окно квоты, предохранитель (circuit breaker)
Лимиты частоты DatabaseRateLimiter (два запроса к php_shield_rate_limit) или RedisRateLimiter (атомарные INCR + EXPIRE, при недоступности Redis пропускает и пишет в лог)
Значения по умолчанию 60 в минуту, 1 000 в час, 10 000 в сутки на адрес и точку входа
Блокировки по тяжести множители: low 0,5 → 30 минут, medium 1 → час, high 4 → 4 часа, critical 12 → 12 часов
Срок за лимит частоты 300 секунд отдельно от блокировок по находкам
Постоянная блокировка только вручную: панель или blockPermanently()

Режимы и наблюдаемость

Режим Поведение
monitor всё пишется в журнал, ничего не блокируется (по умолчанию)
challenge жёсткая находка получает 403 challenge_required без блокировки адреса
block жёсткая находка блокирует адрес на срок по тяжести

Плюс журнал инцидентов с типами и топом адресов, очистка по сроку хранения (SecurityMiddleware::cleanup()), панель с защитой от CSRF, блокировкой входа и перегенерацией сессии, и Support\SecurityHeaders — сборка набора заголовков без их отправки, чтобы это можно было проверять тестами.

Порядок проверок: почему он именно такой

Порядок — не стилистика, а стоимость. Дешёвые шаги стоят впереди, сеть — в конце, а доверенный адрес выходит из обработки до любой работы.

Шаг Что проверяется Почему здесь
1 включён ли файрвол выключенный не должен ничего считать
2 адрес в trusted_ips доверенный не блокируется и не попадает в счётчики
3 активная блокировка повторный запрос не должен снова проходить все проверки
4 счётчик неудач дешёвое чтение состояния
5 лимит частоты подсчёт по окнам, без сети
6 репутация IP сеть: кэш, провайдер, предохранитель
7 поля: GET, POST, REQUEST_URI 6 локальных правил
8 заголовки: User-Agent последним, потому что чаще всего даёт «подозрительно», а не блокировку
flowchart LR
  IN["параметры, сервер, URI"] --> INS["Shield::inspect()"]
  INS --> D{"decision"}
  D -- "blocked=true" --> R1["status 403 или 429, reason"]
  D -- "blocked=false" --> R2["status 200, приложение работает"]
  R1 --> APP["ответ клиенту и запись в журнал"]
  R2 --> APP

Важная деталь: ранний выход работает и против диагноза. В моём третьем прогоне (сканер в User-Agent плюс обход каталогов в поле) в findings оказался только path_traversal — до проверки заголовков обработка не дошла, потому что блокировка уже сработала. Это правильно по смыслу и неудобно при разборе: если хотите увидеть все сработавшие правила сразу, нужен режим monitor.

Решение как данные

inspect() возвращает один и тот же массив на всех ветках:

[
    'blocked'  => true,
    'status'   => 403,
    'reason'   => 'threat_detected',
    'detail'   => 'XSS detected in field "q"',
    'score'    => 90,
    'findings' => ['xss'],
]

Причины (reason) перечислены заранее и не меняются: ip_blocked, too_many_failures, rate_limited, ip_reputation, threat_detected, challenge_required, suspicious_user_agent. Из этого набора получается нормальный лог: по нему видно, что именно произошло, без запуска отладки.

Замеры и цифры

Всё ниже снято мной 08.11.2026 на PHP 8.5.0 (CLI) из локального дерева версии 2.0.0. Замеров два: набор тестов и три запроса через inspect() в режиме block с тестовыми заглушками хранилищ — база не нужна.

Что измерено Значение Как
Набор тестов 141 тест, 333 проверки, OK за 4,577 с php vendor/bin/phpunit (PHPUnit 10.5.66)
Код в src/ 44 файла, 3 911 строк подсчёт файлов и строк
Файлов правил в src/Detection 9 подсчёт файлов
Чистый GET blocked=false, 200, счёт 0, находок нет inspect()
XSS в параметре <script>alert(1)</script> blocked=true, 403, threat_detected, счёт 90, находки xss inspect()
Сканер sqlmap/1.8 плюс ../../../../etc/passwd blocked=true, 403, threat_detected, счёт 60, находки path_traversal inspect()
PHP Shield 2.0.0: что проверено запуском на PHP 8.5

Три вывода. Первый: порог срабатывания виден по счёту — одиночный XSS даёт 90 и блокирует сразу, обход каталогов даёт 60 и тоже блокирует, потому что в режиме block порог находки ниже. Второй: в третьем случае до правила suspicious_header дело не дошло — ранний выход по уже найденной угрозе; это не ошибка, а следствие порядка. Третий: тесты идут 4,6 секунды и не требуют базы, значит их можно ставить в CI без оговорок.

Чего замеры не показывают. Я не поднимал MySQL и Redis: DatabaseRateLimiter и RedisRateLimiter в живом режиме не проверялись, как и репутация через AbuseIPDB (нужен ключ и квота). Панель не открывалась. Всё это — по документации и коду, и в статье помечено как «по описанию», а не «проверено».

Сильные стороны продукта

  • Один ответ вместо трёх подсистем. Решение, причина, счёт и находки приходят из одного места.
  • Порядок зафиксирован. Дешёвые проверки впереди, сеть последней, доверенный адрес не тратит ресурсов.
  • Блокировка зависит от тяжести. 30 минут за мелочь и 12 часов за критичное — вместо одного срока на всё.
  • Лимит за частоту короткий. 300 секунд: офис за NAT не выпадает на час из-за чужого брутфорса.
  • Можно без MySQL. createWithComponents() собирает файрвол из своих компонентов, локальные проверки работают.
  • Работает при отказе сети. Предохранитель и «пропустить при недоступности Redis» — отказ внешнего сервиса не выключает сайт.
  • Проверяемость. SecurityHeaders::build() и заглушки хранилищ позволяют тестировать поведение, а не читать код глазами.
  • Типизированная конфигурация. Неизвестный режим или имя правила падает с ошибкой, а не молча игнорируется.

Установка

composer require paulmann/php-shield

Для проверки из исходников:

git clone --depth 1 --branch v2.0.0 https://github.com/paulmann/PHP-Shield.git
cd PHP-Shield && composer install
php vendor/bin/phpunit
Что нужно Зачем
PHP 8.1–8.5, json, mbstring базовая работа
mysqli хранилище блокировок и лимитер на базе данных
redis атомарный лимитер под нагрузкой
curl запросы репутации в AbuseIPDB

Настройка

flowchart TD
  M["mode: monitor, challenge, block"] --> M1["реакция на находку, а не порядок проверок"]
  S["severity_block_multipliers"] --> S1["low 30 мин, medium 1 ч, high 4 ч, critical 12 ч"]
  R["rate_limit_block_duration"] --> R1["300 с: очередь из NAT не блокирует офис на час"]
  T["trusted_ips"] --> T1["по умолчанию только loopback"]
  L["rate_limits: 60/мин, 1000/ч, 10000/сутки"] --> L1["на адрес и точку входа"]

Минимальный рабочий вариант — режим наблюдения и хранилище в MySQL:

$config = require 'vendor/paulmann/php-shield/config/default.php';
$config['mode'] = 'monitor';           // сначала наблюдение, не блокировка

$mysqli = new mysqli('127.0.0.1', $user, $pass, $db);
$shield = ShieldFactory::create($config, $mysqli);

$decision = $shield->inspect(
    array_merge($_GET, $_POST),
    $_SERVER,
    $_SERVER['REQUEST_URI'] ?? '/'
);

if ($decision['blocked']) {
    http_response_code($decision['status']);
    header('Content-Type: application/json');
    exit(json_encode(['error' => $decision['reason'], 'detail' => $decision['detail']]));
}
Параметр По умолчанию Когда менять
mode monitor после суток наблюдения — на block
trusted_ips только loopback добавить адреса балансировщиков и мониторинга
max_failed_attempts 25 под свой профиль нагрузки
rate_limit_block_duration 300 с не увеличивать без нужды: цена — живые люди за NAT
severity_block_multipliers 0,5 / 1 / 4 / 12 если критичное должно блокировать дольше
rate_limits 60/мин, 1 000/ч, 10 000/сутки под реальный трафик точки входа

Где это встраивается

Три места, где файрвол даёт больше всего за наименьшие усилия.

Фронт-контроллер. Один вызов в начале index.php — до маршрутизации, до подключения базы бизнес-логики. Тогда вредный запрос не доходит до кода, который тратит ресурсы: нет ни подключения к базе, ни рендера, ни вызова API.

Обработчик входа. Счётчик неудачных попыток можно кормить своими событиями: recordFailedAttempt() вызывается из логина, а дальше файрвол сам решает, когда адрес исчерпал терпение. Это лучше, чем отдельный счётчик в сессии: он живёт в базе и виден всем инстансам приложения.

Админка. admin.allowed_ips — грубая, но надёжная защита: ошибка в приложении не откроет панель тому, кто не в списке. Важно помнить порядок: список проверяется до открытия соединения с базой, то есть при недоступной базе панель всё равно откажет чужим.

Место Что даёт Чего не даёт
Фронт-контроллер отсечка до бизнес-логики не заменяет авторизацию
Обработчик входа счётчик попыток в общем хранилище не заменяет пароль и второй фактор
Админка второй слой на входе в панель не защищает от угона сессии
Обратный прокси правильный адрес клиента требует списка прокси в trusted_proxies

Что исправлено в 2.0.0

Раздел нужен тем, кто ставил прежнюю версию под именем SecurityMiddleware: половина пунктов ниже — про обход защиты, а не про удобство.

Что было Что стало
X-Forwarded-For читался слева цепочка читается справа налево: клиент больше не подставляет адрес из белого списка
json_encode(..., JSON_THROW_ON_ERROR) в журнале инцидентов один битый байт давал фатальную ошибку — теперь байты подменяются
mysqli_result::fetch_assoc() без проверки проверка результата, иначе падение при ошибке запроса
Дамп конфигурации в панели целиком хеш пароля и пароль Redis скрыты
Блокировка, разблокировка и выход принимали межсайтовые POST добавлены токены против CSRF
PHPStan уровня max не проходил (50 ошибок) проходит, src/Admin снова анализируется
composer cs-check падал всегда конфиг исправлен, шаг снова обязательный
Схема в migrations/schema.sql расходилась с рантаймом типы и имена индексов сведены
Правило обратных кавычек ловило любой `текст` требует имени команды

Ломающие изменения — тоже часть картины: BlockStoreInterface::block() теперь принимает аргумент тяжести, isRequestAllowed() переехал в Shield, а имя пакета в composer.json стало paulmann/php-shield.

Состояние и панель

Всё, что файрвол знает о адресах, доступно через middleware() — это состояние, а не только решения. Из него берутся ответы на вопросы, которые обычно приходится собирать из нескольких мест: заблокирован ли адрес, сколько у него неудачных попыток, какие блокировки активны и что в статистике.

$state = $shield->middleware();

$state->isIpBlocked('203.0.113.5');
$state->blockIp('203.0.113.5', 'scanner', SecurityMiddleware::SEVERITY_HIGH);
$state->blockPermanently('203.0.113.5', 'manual decision');   // только руками
$state->unblockIp('203.0.113.5');

$state->recordFailedAttempt('203.0.113.5');   // например, из обработчика входа
$state->recordIncident('203.0.113.5', 'login_bruteforce', 'десять неудачных входов', 'high');
$state->activeBlocks(50);
$state->stats();
$state->cleanup(30);        // срок хранения: блокировки, инциденты, счётчики

Полезная деталь устройства счётчиков: когда число неудачных попыток доходит до max_failed_attempts (25), следующий запрос с этого адреса блокируется один раз, а счётчик обнуляется. Иначе адрес оставался бы на грани блокировки навсегда и каждое новое событие повторяло бы её бесконечно.

Пять таблиц базы названы по делу: php_shield_ip_blocks, php_shield_incidents, php_shield_failed_attempts, php_shield_rate_limit, php_shield_ip_reputation. Сроки истечения считает сама база через NOW() и DATE_ADD() — значит разница часовых поясов между PHP и MySQL не может «просрочить» свежую блокировку. Это тот класс ошибок, который иначе ищут неделями.

Панель (public/admin/index.php) показывает статистику, активные блокировки, даёт блокировать и разблокировать вручную и показывает конфигурацию с скрытыми секретами. Защита у неё не декоративная: на каждой меняющей состояние форме свой токен от CSRF, вход ограничен по частоте (пять неудач — пять минут блокировки), идентификатор сессии перегенерируется при входе, cookie — HttpOnly, SameSite=Strict и Secure по HTTPS. Дополнительно есть список разрешённых сетей admin.allowed_ips, выключенный по умолчанию: адрес вне списка получает 403 до того, как панель вообще откроет соединение с базой.

$config['admin']['allowed_ips'] = [
    '203.0.113.0/24',   // офис
    '198.51.100.7',     // административный хост
    '2001:db8::/32',    // IPv6
];

Если приложение стоит за прокси, прокси нужно перечислить в trusted_proxies: без этого пересылаемый заголовок игнорируется и панель видит адрес прокси, а не ваш.

Подводные камни

  • mode = monitor по умолчанию — это не защита, а наблюдение. Если поставить и забыть, блокировок не будет вообще; смотреть надо журнал, а не факт установки.
  • Ранний выход скрывает остальные находки. Проверено: при сканере в User-Agent и обходе каталогов в поле в findings попал только path_traversal. Для разбора нужен monitor.
  • trusted_ips — полный обход. Адрес из списка не блокируется и не считается. Ошибка в списке (например, адрес прокси, за которым сидят все) отключает защиту для всех.
  • Доверие к X-Forwarded-For требует настройки прокси. В 2.0.0 цепочка читается справа налево, но если перед приложением стоит не тот прокси, вы будете определять не того клиента.
  • Лимит частоты — это про адрес, а не про человека. За NAT один нарушитель создаёт нагрузку для всех; срок 300 секунд именно поэтому короткий, и увеличивать его без разбора не стоит.
  • Репутация зависит от внешнего сервиса и квоты. Без ключа проверка не работает; при исчерпании квоты и срабатывании предохранителя решение принимается без неё — это записано в замысле, но требует проверки на своём ключе.
  • Redis-лимитер при недоступности Redis пропускает запросы. Это осознанный выбор в пользу доступности: сайт работает, но лимит временно не действует.
  • Постоянной блокировки автоматически не бывает. Ждать, что «критичное» заблокирует навсегда, нельзя: это ручное действие оператора.
  • Обновление с прежнего имени пакета — ломающее. Меняется имя пакета, подпись block() и место isRequestAllowed(); старый код без правок не соберётся.
  • Табличная схема должна совпадать с рантаймом. В 2.0.0 как раз исправлено расхождение migrations/schema.sql с живыми таблицами: INT против BIGINT UNSIGNED, VARCHAR(50) против 64, другие имена индексов. При обновлении без применения миграций поведение будет отличаться от документации, а ошибки — неочевидными.

Чек-лист внедрения

  • [ ] Проверить версию PHP (8.1–8.5) и расширения json, mbstring.
  • [ ] Поставить пакет и прогнать тесты: php vendor/bin/phpunit (ожидается 141 тест, OK).
  • [ ] Запустить в monitor и оставить на сутки.
  • [ ] Разобрать журнал: какие правила срабатывают, нет ли ложных на обход каталогов у легитимных ссылок.
  • [ ] Задать trusted_ips — только те адреса, которые действительно ваши.
  • [ ] Убедиться, что за прокси определяется настоящий адрес клиента.
  • [ ] Переключить на block и проверить три запроса: чистый, с XSS, со сканером.
  • [ ] Настроить лимиты под свою точку входа, не задирая срок блокировки за частоту.
  • [ ] Включить очистку по сроку (cleanup()) по расписанию.
  • [ ] Защитить панель: пароль, HTTPS, отдельный адрес.

FAQ

Нужен ли MySQL?

Нет. createWithComponents() собирает файрвол из компонентов: локальные правила, заголовки и статистика работают, а хранилище блокировок и лимитер вы подставляете свои. MySQL нужен для ShieldFactory::create() и панели.

Чем это отличается от WAF на веб-сервере?

Тем, что решение принимается в приложении и возвращается данными: видно причину, счёт и конкретные находки. Правила WAF уровня nginx такого ответа не дают, а связка «nginx + PHP» требует двух журналов.

Почему при сканере в User-Agent находка только одна?

Потому что блокировка сработала на предыдущем шаге и обработка завершилась. Порядок намеренный: сначала поля, потом заголовки. Чтобы увидеть все правила сразу, включите monitor.

Что будет, если AbuseIPDB недоступен?

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

Можно ли блокировать навсегда?

Только вручную — из панели или вызовом blockPermanently(). Автоматические блокировки всегда с ограничением по времени, зависящим от тяжести находки.

Почему mode = monitor по умолчанию?

Потому что включение блокировок без наблюдения — это способ заблокировать живых клиентов в первый же день. Сутки в monitor, разбор журнала, потом block.

Итог

PHP Shield 2.0.0 закрывает скучную, но нужную часть веб-безопасности: единый порядок проверок, решение в виде данных и предсказуемые сроки блокировок. Главное в нём — не список правил, а дисциплина: доверенные адреса выходят рано, сеть спрашивается последней, лимит за частоту короткий, а постоянная блокировка оставлена человеку. Из исправлений 2.0.0 важнее всего чтение X-Forwarded-For справа налево — именно эта ошибка превращала белый список в способ обойти защиту целиком. Мои замеры: 141 тест зелёный, чистый запрос проходит, XSS даёт 403 и 90 баллов, обход каталогов — 403 и 60, а база и Redis в этих прогонах не участвовали и остаются на проверку в бою.

Цитата: Файрвол ценен не тем, что блокирует, а тем, что может объяснить блокировку — иначе через месяц его отключают, чтобы «не мешал».

Связанные материалы

LLM Summary

  • Тема: PHP Shield 2.0.0 — модульный файрвол для PHP-запросов: локальные правила, репутация IP, лимиты частоты, блокировки, журнал и панель.
  • Точка входа: Shield::inspect() возвращает массив blocked, status, reason, detail, score, findings; isRequestAllowed() — булев фасад.
  • Порядок: включённость → trusted_ips → активная блокировка → счётчик неудач → лимит частоты → репутация IP → проверка полей (6 правил по GET, POST, REQUEST_URI) → заголовки.
  • Правила и счёт: sql_injection 25–80, xss 40–90, command_injection 60–85, path_traversal 60–95, file_inclusion 60–85, suspicious_header 60–70.
  • Режимы: monitor (по умолчанию), challenge, block; сроки блокировки = базовый × множитель тяжести: low 0,5 (30 мин), medium 1 (1 ч), high 4 (4 ч), critical 12 (12 ч); за лимит частоты — 300 с.
  • Замеры на PHP 8.5.0 (08.11.2026): phpunit — 141 тест, 333 проверки, OK за 4,577 с; src/ — 44 файла, 3 911 строк; inspect() в режиме block: чистый GET 200/0/без находок, XSS 403/90/xss, сканер плюс ../../../../etc/passwd 403/60/path_traversal.
  • Наблюдение: при уже сработавшей блокировке обработка завершается раньше проверки заголовков — в находках остаётся только первое правило.
  • Не проверялось: MySQL-хранилище, Redis-лимитер, живая репутация AbuseIPDB, панель; это по документации и коду.
  • Ссылки: https://github.com/paulmann/PHP-Shield, https://github.com/paulmann/PHP-Shield/releases/tag/v2.0.0

Промт для агента (установка и настройка)

Готовый промт для AI-агента: скопируйте блок целиком. Шаги опираются на README.md репозитория и его AGENTS.md (контракт для агентов), формулировки сверены с кодом версии 2.0.0.

Задача: подключить PHP Shield к PHP-приложению и доказать, что он различает чистый запрос, XSS и обход каталогов.

Материалы:
- репозиторий: https://github.com/paulmann/PHP-Shield
- README (порядок проверок, режимы, лимиты): https://github.com/paulmann/PHP-Shield/blob/main/README.md
- контракт для агентов: https://github.com/paulmann/PHP-Shield/blob/main/AGENTS.md
- релиз: https://github.com/paulmann/PHP-Shield/releases/tag/v2.0.0

Шаг 1. Проверить окружение:
php -v                                  # 8.1–8.5
php -m | grep -E 'json|mbstring'        # обязательные расширения

Шаг 2. Установка:
composer require paulmann/php-shield
php vendor/bin/phpunit                  # ожидается 141 тест, OK

Шаг 3. Сборка без базы (для проверки поведения):
$config = require 'config/default.php';
$config['mode'] = 'block';
$shield = ShieldFactory::createWithComponents($blockStore, $rateLimiter, $config);
$decision = $shield->inspect($_GET + $_POST, $_SERVER, $_SERVER['REQUEST_URI'] ?? '/');

Шаг 4. Три контрольных запроса: чистый (ожидание 200, score 0), с <script>alert(1)</script> (403 threat_detected, score 90), со сканером sqlmap и ../../etc/passwd (403, score 60).
Шаг 5. Включить mode=monitor на сутки, разобрать журнал, затем переключить на block.
Шаг 6. Задать trusted_ips (только свои адреса), проверить определение клиента за прокси.
Шаг 7. Настроить лимиты и очистку по сроку (cleanup()).

Запрещено:
- ставить в эксплуатацию сразу в block без суток в monitor;
- вносить в trusted_ips адреса прокси, за которыми сидят все клиенты: запись обходит все проверки;
- увеличивать rate_limit_block_duration без разбора: за NAT пострадают все;
- читать X-Forwarded-For слева: цепочка читается справа налево, иначе клиент подставит чужой адрес;
- считать блокировку постоянной: автоматические блокировки всегда с ограничением по времени.

Что считать успехом: чистый запрос проходит, XSS и обход каталогов получают 403 с записанными reason и score, в журнале видно причину блокировки, тесты зелёные.