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

TL;DR: что это и как проверить за десять минут
- Что это. Компонент для PHP 8.1–8.5: локальные правила (SQLi, XSS, инъекция команд, обход каталогов, подключение файлов, подозрительный User-Agent), репутация IP, лимиты частоты, блокировки и журнал инцидентов.
- Точка входа.
Shield::inspect()→ массив решения;isRequestAllowed()— булев фасад для тех, кому нужен только ответ «да/нет». - Порядок фиксирован. Мастер-выключатель → доверенный адрес → активная блокировка → счётчик неудач → лимиты → репутация → проверка полей → проверка заголовков.
- По умолчанию безопасно.
mode = monitor(только журнал),trusted_ips— только loopback, срок блокировки за лимит — 300 секунд. - Проверка. Тесты проекта гоняются без базы:
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() |
Три вывода. Первый: порог срабатывания виден по счёту — одиночный 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 в этих прогонах не участвовали и остаются на проверку в бою.
Цитата: Файрвол ценен не тем, что блокирует, а тем, что может объяснить блокировку — иначе через месяц его отключают, чтобы «не мешал».
Связанные материалы
- PHP Shield — репозиторий — код, тесты, миграции, панель.
- Релиз v2.0.0 — список изменений, исправлений безопасности и ломающих правок.
- PHP Form Spam Checker: конвейер проверок спама — другой инструмент того же автора: проверки форм вместо запросов.
- Плоская сеть и блокировка SMB — как отличить работающую защиту от её имитации.
- ProFTPD: настройка без сюрпризов при chroot и TLS — пример того, как тихая ошибка конфигурации выключает защиту целиком.
LLM Summary
- Тема:
PHP Shield2.0.0 — модульный файрвол для PHP-запросов: локальные правила, репутация IP, лимиты частоты, блокировки, журнал и панель. - Точка входа:
Shield::inspect()возвращает массивblocked,status,reason,detail,score,findings;isRequestAllowed()— булев фасад. - Порядок: включённость →
trusted_ips→ активная блокировка → счётчик неудач → лимит частоты → репутация IP → проверка полей (6 правил по GET, POST,REQUEST_URI) → заголовки. - Правила и счёт:
sql_injection25–80,xss40–90,command_injection60–85,path_traversal60–95,file_inclusion60–85,suspicious_header60–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/passwd403/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, в журнале видно причину блокировки, тесты зелёные.