Дата: 2026-06-03
Версия PHP: 8.x
Файлов: 96 .php
Фреймворк: Fury (кастомный MVC)
Каждая фаза — законченный пакет работ. Внутри фазы задачи отсортированы по приоритету (критичные → высокие → средние → низкие). Фазы следуют друг за другом: нет смысла начинать Phase 3, пока не закрыта Phase 1.
Приоритеты:
Цель: Закрыть векторы, через которые злоумышленник может получить полный доступ к системе.
Коммит: 175224e (ветка dev)
Блокер для следующих фаз: нет смысла строить валидацию и обработку ошибок поверх дыр в аутентификации и SQL.
Где: server/SHServ/Routes.php:58-82
Что: Все endpoint'ы /api/v1/* открыты без проверки сессии/токена. Любой HTTP-запрос может управлять устройствами, менять скрипты, перезагружать реле.
Фикс:
Routes.php (или App.php) перед роутингом.Authorization: Bearer <token> или Cookie: auth_token=<token>.POST /events/new (устройства шлют без сессии, но с device token)./dev/test/*) если devmode === false.Где:
server/Fury/Modules/ThinBuilder/ThinBuilderProcessing.php:40-108server/Fury/Modules/ThinBuilder/ThinBuilder.php:58-87Что: addslashes() используется как основной механизм экранирования. Это недостаточно для MySQL (возможен multibyte-обход). Все insert, update, delete и where собирают raw SQL строки конкатенацией.
Фикс:
ThinBuilder на PDO prepared statements.? плейсхолдеры + bindValue().Где: server/SHServ/config.php:6,13-14,20
Что:
debug => true и devmode => true в коммите — стектрейсы и dev-tools доступны на проде.db.user = "eugene", db.password = "root" в plaintext в git.Фикс:
.env файл на уровне server/ (добавить в .gitignore).vlucas/phpdotenv или простой парсер.config.php оставить как config.example.php с placeholder'ами.config.php → .env-based загрузку.debug = false, devmode = false. Переопределять через env.Где: server/SHServ/Models/Example_Auth.php:24
Что: Пароли хешируются sha1(), который криптографически сломан и быстрый к брутфорсу.
Фикс:
password_hash($password, PASSWORD_ARGON2ID) при регистрации/смене.password_verify($password, $hash) при входе.password_hash, при первом успешном password_verify обновлять из sha1.Где: server/SHServ/Sessions.php:13
Что: Токен генерируется uniqid($uid . time()) — низкая энтропия, предсказуем.
Фикс:
$token = bin2hex(random_bytes(32));
Где: server/SHServ/Sessions.php:38
Что: setcookie("auth_token", ...) без HttpOnly, Secure, SameSite. Увеличивает impact XSS.
Фикс:
setcookie("auth_token", $token, [
'expires' => time() + 86400 * 30,
'path' => '/',
'httponly' => true,
'secure' => true, // если HTTPS
'samesite' => 'Strict',
]);
get.config сливает конфигГде: server/console.php:10-12
Что: Команда get.config выводит весь массив FCONF (включая пароль БД) без аутентификации.
Фикс: Удалить команду или закрыть проверкой локального файла-ключа (например, только если php_sapi_name() === 'cli' и uid = владелец файла).
Цель: Убрать silent failures, сделать транзакции атомарными, починить инвертированную логику error handler'а.
Коммит: 1a30037 (ветка dev)
Блокер: пока error handler инвертирован, трудно доверять логам и репортам.
Где: server/Fury/Modules/ErrorHandler/ErrorHandler.php:26-29
Что:
if(!FCONF["debug"]) {
error_reporting(-1);
}
Ошибки показываются, когда debug выключен. Логика наоборот.
Фикс: Поменять ветки — показывать error_reporting(-1) когда debug === true, подавлять (или логировать) когда false.
Где: server/Fury/Modules/ErrorHandler/ErrorHandler.php:41
Что: set_exception_handler закомментирован — uncaught exceptions утекают со стектрейсами.
Фикс: Раскомментировать и направлять через exception_handler.
Где: server/SHServ/Models/Devices.php:30-82
Что: connect_new_device делает два INSERT'а (devices, device_auth) без транзакции. Если второй упадёт, первый остаётся сиротой. Есть fallback-удаление, но оно не сработает при fatal error между INSERT'ами.
Фикс:
$db->beginTransaction();
try {
// insert devices
// insert device_auth
$db->commit();
} catch (\Exception $e) {
$db->rollBack();
throw $e;
}
null на SQL-ошибкахГде: server/Fury/Modules/ThinBuilder/ThinBuilder.php:28-30
Что: query() возвращает null при ошибке SQL, не логируя и не бросая исключение. TODO-комментарий подтверждает, что это известный баг.
Фикс:
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION в DB.php.return null — пусть PDO бросает.try/catch в контроллере, маппинг в response_error.response_error должен ставить HTTP-статусГде: server/SHServ/Utils.php:14-30
Что: response_error() всегда отдаёт HTTP 200 OK. Клиенты (и прокси) не могут отличить ошибку от успеха по статусу.
Фикс:
function response_error($msg, $alias = "error", $status_code = 400) {
http_response_code($status_code);
// ...
}
Контроллеры передают подходящий код: 400 для клиентских, 500 для серверных.
Где:
server/SHServ/Models/Areas.php:103-105 — remove_obsolete() пустойserver/SHServ/Helpers/Validator.php — класс пустой, не используетсяФикс: Реализовать или удалить.
Цель: Валидация входных данных, единообразные ответы, защита от abuse.
Коммит: 35f9ec8 (ветка dev)
Блокер: валидация входных данных бессмысленна, если за ней всё равно стоит уязвимый ThinBuilder (Phase 1). Поэтому Phase 3 идёт после Phase 1.
Где:
server/SHServ/Controllers/AreasRESTAPIController.php:52-77 — new_area не валидирует type, alias, display_nameserver/SHServ/Controllers/DevicesRESTAPIController.php:32 — IP проверяется только strlen < 7server/SHServ/Controllers/ScriptsRESTAPIController.php:19-29 — run_action_script передаёт сырые $params в callable без валидацииserver/SHServ/Controllers/DevicesRESTAPIController.php:206 — do_device_action передаёт сырые $params на устройствоФикс:
Validator (или использовать filter_var + regex).alias — /^[a-z0-9_]+$/, display_name — max 255, type — whitelist.filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4).$params — whitelist по action alias или JSON Schema.scope_file возвращает не JSONГде: server/SHServ/Controllers/ScriptsRESTAPIController.php:59-79
Что: На success возвращает raw PHP source code, на error — JSON. Клиент должен угадывать формат.
Фикс: Всегда JSON-обёртка:
{ "status": true, "data": { "source": "<?php ..." } }
Где: server/SHServ/Controllers/ScriptsRESTAPIController.php:81-102
Что: Принимает $path и $file от клиента, пишет на диск с минимальной проверкой (strpos($filepath, ".php")). Можно писать вне ControlScripts/Scopes/.
Фикс:
__DIR__ . "/../../ControlScripts/Scopes/".realpath($filepath) начинается с разрешённого префикса.set_script_state молча проглатывает невалидные значенияГде: server/SHServ/Controllers/ScriptsRESTAPIController.php:110-120
Что: Любое значение, не равное "enable", трактуется как disable. Невалидный запрос ("foobar") получает success.
Фикс:
if (!in_array($state, ["enable", "disable"], true)) {
return response_error("Invalid state", "invalid_state", 400);
}
Где: server/SHServ/Routes.php:58-82
Что: Нет rate limiting. Можно спамить do_device_action, /events/new, сканирование.
Фикс: Простой in-memory rate limiter (на основе IP) или nginx limit_req для начала. Для критичных endpoint'ов — 10 req/sec per IP.
Цель: Сделать работу с устройствами отказоустойчивой и неблокирующей.
Коммит: b4968d4 (ветка dev)
Где: server/SHServ/Tools/DeviceScanner.php:72-98
Что: scan_ips создаёт curl handle для каждого IP диапазона. Для /24 — 253 хендла одновременно. Может исчерпать fd и память.
Фикс: Batch-сканирование, sliding window (например, 32 параллельных хендла максимум). Остальные ставить в очередь.
Где: server/SHServ/Tools/DeviceAPI/Base.php:138-226
Что: Один transient network failure = полный провал запроса. Нет повторных попыток.
Фикс: Добавить retry loop (3 попытки, backoff 100ms, 300ms). Только для идемпотентных операций (status, action). POST на /setup — без retry или с idempotency key.
Где: server/SHServ/Tools/DeviceAPI/Base.php:178-179
Что: CONNECTTIMEOUT => 1, TIMEOUT => 5 — не конфигурируются.
Фикс: Вынести в FCONF['device_api_timeout'] или device-level config.
Где:
server/SHServ/Controllers/DevicesRESTAPIController.php:177 — device_statusserver/SHServ/RequiredControlScriptsScope.php:12-21 — online event handler вызывает get_about()Что: PHP-поток блокируется на время cURL timeout. Если устройство offline, ждём полные 5 секунд.
Фикс:
curl_multi (уже есть в DeviceScanner, переиспользовать).reset_device игнорирует ответ устройстваГде: server/SHServ/Controllers/DevicesRESTAPIController.php:382
Что: Вызывает $device->device_api()->reset() и не проверяет результат. Если устройство offline, клиент получает "success".
Фикс: Проверять http_code == 200, иначе device_request_fail.
Цель: Убрать мёртвый код, дедупликацию, странные сайд-эффекты.
Коммит: d9c9e17 (ветка dev)
Можно выполнять параллельно с Phase 2–4, но не раньше Phase 1.
Где: server/SHServ/App.php:35-36
Что: На каждый HTTP-запрос принудительно включается spotlights_off action script. Это не должно жить в bootstrap.
Фикс: Вынести в миграцию/seed-скрипт, выполняемый один раз при установке.
place_in_area / unassign_from_areaГде:
server/SHServ/Controllers/DevicesRESTAPIController.php:238-279server/SHServ/Controllers/AreasRESTAPIController.php:108-129Что: Логика размещения устройства в area дублируется почти дословно.
Фикс: Вынести в сервис AreaPlacementService или shared validator.
is_dir($item) проверяет CWD, не Scopes/Где: server/SHServ/App.php:92
Что:
array_filter($scripts_dir, function($item) {
return !is_dir($item) and ...;
});
$item — имя без пути, is_dir проверяет текущую директорию, а не ControlScripts/Scopes/.
Фикс:
!is_dir(__DIR__ . "/../ControlScripts/Scopes/" . $item)
$device вместо $scriptГде: server/SHServ/Controllers/ScriptsRESTAPIController.php:143,146-148
Что: Переменная называется $device, но содержит Script. На ошибке возвращается alias "device_not_found" вместо "script_not_exists".
Фикс: Переименовать и исправить alias.
User.php конструирует Profile с $uidГде: server/SHServ/Entities/User.php:19
Что: new Profile($uid) — вместо profile_id передаётся uid. TODO-комментарий подтверждает баг.
Фикс: Передать реальный profile_id (запросить из БД или взять из поля profile_id).
Где: server/SHServ/Tools/DeviceAPI/Hatch.php:21,29,37,45
Что: Методы is_opened, is_closed, is_opening, is_closing ссылаются на $status_response, которой нет в scope.
Фикс: Использовать $state или корректное имя переменной.
Где:
server/ControlScripts/Scopes/OfficeRoomScope.php:71-98server/ControlScripts/Scopes/SpotlightsScope.php:127-160server/ControlScripts/Scopes/TestScriptsScope.php:31-71Что: Большие закомментированные блоки.
Фикс: Удалить или перенести в docs/notes.md.
ControlScripts/Common.phpГде: server/ControlScripts/Common.php:8-123
Что: Десятки device alias'ов (spotlight_main_back_1, buttons_backdoor...) зашиты в код.
Фикс: Вынести sync map в БД или dedicated YAML/JSON конфиг.
root_folder() хрупкийГде: server/SHServ/App.php:63-65
Что:
list($root) = explode('SHServ', __DIR__);
Сломается, если директория переименуется.
Фикс:
return dirname(__DIR__, 2);
get_all() magic numberГде: server/SHServ/Models/Areas.php:92
Что: Хардкод limit 1000.
Фикс: Константа AREAS_MAX_LIMIT или pagination.
Где: server/Fury/Kernel/Logging.php:88-96
Что: Логи пишутся в SHServ/Logs/ (под web root), chmod 0755, нет flock.
Фикс:
chmod 0640.flock() или log-rotate.PHPUnit установлен в server/ (composer.json). Для тестирования ThinBuilder добавлена поддержка SQLite (:memory:) в ThinBuilderProcessing::create_connect().
cd server ./vendor/bin/phpunit
| Тест | Что проверяет | Статус |
|---|---|---|
ThinBuilderTest |
insert, select (flat + nested where), update, delete, SQLi identifier/value, транзакции, SQL generation | ✅ 10 тестов |
RateLimiterTest |
лимиты, блокировка, сброс окна, независимость по IP | ✅ 4 теста |
PasswordHashTest |
Argon2id verify, SHA1 legacy fallback, rehash detection | ✅ 3 теста |
EntityCrudTest |
Entity::update(), get(), remove_entity(), id(), to_array() |
✅ 6 тестов |
AreaPlacingTest |
Area::place_in_area(), place_in_area_id() |
✅ 2 теста |
AreaRecursionTest |
get_inner_areas() recursive/non-recursive, depth limit ≤10, remove() cascade, get_inner_devices/scripts, parent_area() |
✅ 10 тестов |
AreasRESTAPIControllerValidationTest |
new_area, update_alias, remove_area, place_in_area, update_display_name validation |
✅ 10 тестов |
DevicesRESTAPIControllerValidationTest |
setup_new_device, do_device_action, update_alias, devices_list, place_in_area validation |
✅ 12 тестов |
ScriptsRESTAPIControllerValidationTest |
run_action_script, set_*_state, place_in_area validation |
✅ 10 тестов |
SessionsTest |
create(), get_session_by_token(), close(), статус |
✅ 4 теста |
UtilsTest |
response_error, response_success, table_row_is_exists, generate_token, dayname_translate, fast_ping_tcp |
✅ 7 тестов |
DeviceAPIBaseRetryTest |
retry/backoff с mock cURL, invalid JSON, get_status non-200, token requirement |
✅ 7 тестов |
AppAuthGuardTest |
check_api_auth() — Bearer/Cookie token, missing/invalid token, rate limit 429, window reset |
✅ 7 тестов |
DevicesModelTransactionTest |
connect_new_device happy path, rollback, setup mode check, device not found |
✅ 4 теста |
AreasControllerHappyPathTest |
new_area, areas_list, update_display_name, update_alias, remove_area, place_in_area, exists_types |
✅ 7 тестов |
ScriptsModelStateTest |
get_scopes_list, script_state, enable/disable_script, set_script_state errors |
✅ 8 тестов |
Итого: 113 тестов, 285 ассертов — все проходят.
DeviceAPI\Base happy-path с реальным HTTP (интеграционные).Models\Devices — remove_device, reboot_device happy-path.App::api_auth_guard() exit/headers integration (end-to-end).EventsController валидация.DeviceScanner batch scanning.Цель: Повторное ревью слоя аутентификации и интеграции с устройствами после внедрения тестов.
Коммит: — (ветка dev)
Этот ревью выполнен после добавления 113 тестов. Некоторые проблемы обнаружены при анализе кода для тестирования.
Где: server/SHServ/Tools/RateLimiter.php
Что: static $requests — in-memory property процесса. При PHP-FPM каждый запуск — новый процесс, счётчик сбрасывается. Rate limiting фактически отсутствует.
Фикс: Перенести на Redis / memcached / DB, или file-based storage с flock().
/events/new полностью открытГде: server/SHServ/Routes.php:77-81, server/SHServ/Controllers/EventsController.php
Что: check_api_auth() пропускает URI, не начинающиеся с /api/v1/. /events/new — вне этого префикса, поэтому endpoint публичен. Любой может отправить события от имени любого устройства, зная device_hard_id.
Фикс: Добавить Authorization: Bearer <device_token> и проверять его в EventsController::new_event перед by_hard_id, или перенести endpoint под /api/v1/.
connect_new_device()Где: server/SHServ/Models/Devices.php:11-86
Что: Нет atomic проверки статуса устройства. Два параллельных запроса могут одновременно увидеть "setup", создать дубли в devices+device_auth. Устройство перейдёт в normal mode, но в БД останутся дубли.
Фикс: Внутри транзакции connect_new_device() выполняет SELECT ... FOR UPDATE (MySQL) / обычный SELECT (SQLite) по device_hard_id + status='active'. Дубль отклоняется до INSERT. Также добавлен UNIQUE(device_hard_id, status) в тестовую схему. Статус: ✅ Исправлено.
Entity::update() unconditionally требует update_atГде: server/SHServ/Middleware/Entity.php:67-79
Что: Entity::update() всегда inject-ит update_at в UPDATE SQL. DeviceAuth (таблица device_auth) не содержит update_at в $fields и скорее всего нет в production schema. Вызов DeviceAuth::kill() → update() вызовет SQL error.
Фикс: Entity::update() теперь проверяет in_array($this->field_name_of_update_at, static::$fields, true) перед инъекцией. Статус: ✅ Исправлено.
Где: server/SHServ/Models/Devices.php:73-76
Что: Транзакция commit-ится, а затем вызывается $device->set_device_token() и $device->device_api()->set_device_name(). Если устройство offline после commit — в БД зарегистрировано, но физически не знает токен/имя. Управление невозможно.
Фикс: Либо remote setup до commit (rollback при fail), либо флаг setup_pending + background retry job.
DeviceAuth::kill() не инвалидирует кэшГде: server/SHServ/Entities/DeviceAuth.php:23-26, server/SHServ/Entities/Device.php:45-63
Что: Device::auth() кэширует device_auth_instance. После $device_auth->kill() статус меняется на "killed", но кэш в $device->device_auth_instance остаётся со старым статусом "active". Последующие вызовы $device->auth()->is_active() вернут true.
Фикс: DeviceAuth::kill() вызывает $device->clear_device_auth_cache() через back-reference, переданную при создании инстанса. Статус: ✅ Исправлено.
/events/newГде: server/SHServ/Controllers/EventsController.php:17-57
Что: Устройство доказывает идентичность только через device_hard_id в POST body. Нет HMAC, timestamp, nonce. Replay attack возможен.
Фикс: Требовать Authorization: Bearer <device_token> (см. 6.2) и/или подпись полезной нагрузки.
generate_token(16) — низкая энтропия для device_tokenГде: server/SHServ/Utils.php:144-149
Что: 16 hex-символов = 64 бита. Для локальной сети терпимо, но на грани.
Фикс: Поднять до 32 (128 бит) для device_token.
$ip_address в DeviceAPI\BaseГде: server/SHServ/Tools/DeviceAPI/Base.php:18-24
Что: http:// + произвольная строка. Если $ip_address содержит path — SSRF возможен (хотя контролируется server-side).
Фикс: Валидировать filter_var($ip_address, FILTER_VALIDATE_IP) в конструкторе.
Entity::update() — тихий success при SQL error?Где: server/SHServ/Middleware/Entity.php:67-79
Что: Entity::update() вызывает $this->thin_builder()->update(...) и не проверяет return value. ThinBuilder::update возвращает Statement или null. Возможно exception вылетит (PDO ERRMODE_EXCEPTION), но если нет — тихий fail.
Фикс: Явно проверять результат и возвращать false при ошибке.
Sessions::get_current_session() — лишний UPDATEГде: server/SHServ/Sessions.php:82-96
Что: Обновление last_using_at при каждом запросе создаёт write load. При масштабе заметно.
Фикс: Обновлять раз в N минут (например, если delta > 60s).
EventsController — set_time_limit(10) обрезает handlersГде: server/SHServ/Controllers/EventsController.php:30
Что: Фоновая обработка после flush (channel_alias_device_event_call и др.) может занять >10s — fatal.
Фикс: set_time_limit(0) или background queue (cron / worker).
Где: server/SHServ/App.php:87-89
Что: substr($auth_header, 7) без проверки длины. Header "Bearer" (без пробела) даст пустой token, который пройдёт в !$token → 401. Не критично.
Фикс: Использовать str_starts_with + explode(' ', $auth_header, 2)[1] ?? null.
Цель: Проверить модели, Entity-классы, ORM ThinBuilder, фабрики и хелперы на корректность, согласованность и производительность.
Ветка: dev
Продолжение ревью после Phase 6. В этой фазе найдено 34 находки: 5 критических, 14 высокого приоритета, 15 среднего/информационного.
Где: server/SHServ/Middleware/Entity.php:36-45
Что: Если записи с указанным id нет, select() возвращает [], и list($this->data) = [] устанавливает $this->data = null. Поле $was_filled всё равно становится true. При первом доступе к свойству через __get() → get() происходит обращение $this->data[$field_name] где data === null — fatal error.
Фикс: Проверять результат select() и бросать исключение, если строка отсутствует. Статус: ✅ Исправлено.
Где: server/SHServ/Entities/DeviceAuth.php:7-9, server/SHServ/Middleware/Entity.php:67-79
Что: Entity::update() безусловно inject-ит update_at в UPDATE SQL. DeviceAuth::$fields не содержит update_at, а production-схема device_auth (в отличие от тестовой) скорее всего не имеет этой колонки. Тесты (DevicesModelTransactionTest.php) создают таблицу с update_at, поэтому тесты проходят, но в production kill() бросит PDOException.
Фикс: Entity::update() теперь проверяет in_array($this->field_name_of_update_at, static::$fields, true) перед инъекцией update_at. Статус: ✅ Исправлено (см. 6.4).
Где: server/SHServ/Entities/Device.php:36-39
Что: Метод меняет status устройства на "removed", но не трогает таблицу device_auth. Строка авторизации остаётся со статусом active, и токен продолжает считаться валидным.
Фикс: В remove() дополнительно вызывать $this->auth()->kill() или делать операцию транзакционной. Статус: ⏸️ Отложено. Будет решено вместе с системой авторизации / RBAC (только админ может удалять устройства).
Где: server/SHServ/Entities/Area.php:267-282
Что: update() на devices (area_id = 0), update() на areas (parent_id = 0), и remove_entity() — три независимых запроса без beginTransaction. При падении после первого или второго остаются битые ссылки.
Фикс: Обернута в beginTransaction/commit/rollBack. Статус: ✅ Исправлено.
Где: server/SHServ/Models/Scripts.php:63-73
Что: При $uniq_names = [] генерируется ... WHERE type = ? AND uniq_name IN () — синтаксическая ошибка SQL.
Фикс: Ранний возврат [] если массив пуст. Статус: ✅ Исправлено.
Где: server/Fury/Modules/ThinBuilder/ThinBuilderProcessing.php:147-156
Что: Любая строка проходит как оператор прямо в SQL. Значения параметризованы, но оператор — сырой текст.
Фикс: Whitelist допустимых операторов: =, !=, <>, <, >, <=, >=, LIKE, IN, IS, NOT IN, BETWEEN. Статус: ✅ Исправлено.
Где: server/Fury/Modules/ThinBuilder/ThinBuilderProcessing.php:62-67
Что: Регулярка /^[a-zA-Z0-9_]+$/ пропускает 123abc, что недопустимо в SQL. Правильная: /^[a-zA-Z_][a-zA-Z0-9_]*$/.
Фикс: /^[a-zA-Z_][a-zA-Z0-9_]*$/. Статус: ✅ Исправлено.
Где: server/Fury/Modules/ThinBuilder/ThinBuilderProcessing.php:69-79
Что: Метод использует addslashes() вместо prepared statements. Нигде не вызывается, но его наличие создаёт риск.
Фикс: Удалить метод. Статус: ✅ Исправлено.
Где: server/SHServ/Models/MetaManager.php:75-86
Что: SELECT, затем INSERT или UPDATE. При конкурентных запросах возможен дубль.
Фикс: Обернуто в beginTransaction/commit/rollBack. Статус: ✅ Исправлено.
Где: server/SHServ/Entities/User.php:16-28
Что: Каждое создание User инициирует дополнительный SELECT из profiles. N+1 при массовой выборке.
Фикс: Ленивая загрузка через get_pet_instance.
Где: server/SHServ/Models/Scripts.php:29-51
Что: unlink() выполняется до $script->remove(). Если DB delete упадёт, файл уже удалён.
Фикс: Удалять файл после успешного DB delete. Статус: ✅ Исправлено.
Где: server/SHServ/Middleware/Entity.php:67-79
Что: ThinBuilder::update() возвращает rowCount(), но Entity игнорирует его. Если UPDATE не затронул строк, всё равно true.
Фикс: Проверять rowCount() > 0.
Где: server/Fury/Modules/ThinBuilder/ThinBuilderProcessing.php:149-151
Что: count("string") в PHP 8 бросает TypeError. Если по ошибке передать скаляр в IN, приложение упадёт.
Фикс: Проверка is_array($w_item[2]). Статус: ✅ Исправлено.
Где: server/Fury/Modules/ThinBuilder/ThinBuilderProcessing.php:115-130
Что: ['field', ['v1', 'v2']] превращается в ['field', '=', ['v1', 'v2']] из-за count === 2. Массив пытается забиндиться как скаляр.
Фикс: Обрабатывать массив второго элемента как IN. Статус: ✅ Исправлено.
Где: server/SHServ/Middleware/Entity.php:67-79
Что: При PDOException modified_fields остаётся заполненным. Повторный update() запишет устаревшие + новые изменения.
Фикс: try/finally — сбрасывать только при успехе.
Где: server/SHServ/Entities/Device.php:94-102
Что: auth() вызывается трижды внутри одного метода.
Фикс: Закэшировать $auth = $this->auth().
Где: server/SHServ/Models/Scripts.php:75-128
Что: Reflection на каждый вызов для scopes. Для regular/action — O(n·m) линейный поиск.
Фикс: Хеш-таблица keyed by uniq_name.
Где: server/SHServ/Models/Areas.php:36-57
Что: Запрашивает все строки, затем дедуплицирует в PHP.
Фикс: SELECT DISTINCT type FROM areas.
Где: server/SHServ/Entities/Area.php:129-158
Что: if($lvl >= 10) { return []; } — молча отрезает хвост при глубоком дереве.
Фикс: Исключение при превышении.
Где: server/Fury/Modules/ThinBuilder/ThinBuilder.php:197-213
Что: explode('(', $raw_field[1]) для INT, TEXT, DATETIME возвращает массив из одного элемента. list() присвоит null.
Где: server/Fury/Modules/ThinBuilder/ThinBuilder.php:215-222
Что: SHOW TABLES — MySQL-специфичный синтаксис. При SQLite не работает.
Где: server/tests/DevicesModelTransactionTest.php:44-52
Что: Тесты создают device_auth с update_at TEXT, но production schema может отличаться. Нет единого источника правды для схемы.
Фикс: Создать schema.sql в репозитории.
Где: Все модели
Что: Уникальность проверяется через count() на уровне приложения. Race conditions возможны.
Фикс: Добавить UNIQUE INDEX на areas.alias, devices.alias, devices.device_hard_id (active), users.alias/email, meta.(assignment, ent_id, name).
Где: server/SHServ/Entities/Area.php:68-71, server/SHServ/Entities/Traits/AreaPlacing.php:12-15
Что: Идентичные методы. Класс побеждает trait, но дублирование затрудняет поддержку.
Где: server/SHServ/Factory/Creator.php:32-50
Что: Вставляет фиктивное update_at только чтобы удовлетворить Entity::update().
Где: server/SHServ/Factory/Getter.php:25-39
Что: Запрашивает только id, затем создаёт Profile без данных, форсируя ленивую загрузку.
Где: server/SHServ/Middleware/Model.php:8-13
Что: using_model() — лишний вызов в production.
Где: server/SHServ/Models/Areas.php:20
Что: "parent_id" => "0" — string вместо int.
Где: server/SHServ/Helpers/MetaWrap.php:9
Что: Глобальное mutable состояние.
Где: server/Fury/Modules/ThinBuilder/ThinBuilder.php:17-42
Что: Метод принимает строку SQL и выполняет напрямую. Опасен при использовании с пользовательским input.
Где: server/SHServ/Models/Scripts.php:170-184
Что: array_filter внутри цикла. При 50 скриптах — 2500 сравнений.
Фикс: Построить array_reduce → keyed lookup.
Где: Models/Areas.php, Models/Devices.php
Что: Areas::create_new_area() → Area|null, Devices::connect_new_device() → Device|Array.
Фикс: Единый Result-объект или Exception-based flow.
Где: server/SHServ/Models/Areas.php:27-34
Что: При использовании для update собственный alias будет засчитан как дубль.
Где: server/SHServ/Middleware/Entity.php:85-87
Что: remove_entity() использует плоский массив ["id", "=", $this->id()] вместо nested. Работает, но не единообразно.
Дата: 2026-06-02
Полный отчёт: server/docs/review-phase-3-controllers-routing-validation.md
Где: Fury/Modules/Router/RouterImplementation.php
Что: URI_routing() и GET_and_POST_routing() не делают break после первого совпадения. Один HTTP-запрос может вызвать несколько контроллеров.
Статус: ⚠️ By design / intentional. Автор подтвердил, что это ожидаемое поведение фреймворка.
Где: SHServ/Routes.php:61-62
Что: /cron/regular-scripts и /cron/status-update-scanning — публичные URI-роуты. Любой может триггерить сканирование сети и выполнение регулярных скриптов.
Фикс: CronController::ensure_localhost_only() — только 127.0.0.1 / ::1.
Статус: ✅ Исправлено.
Где: Fury/Modules/Router/RouterImplementation.php:61-91
Что: Роутер проверяет только наличие параметров в $_GET/$_POST, но не $_SERVER['REQUEST_METHOD']. Все деструктивные POST-эндпоинты доступны через GET-ссылку.
Фикс: GET_and_POST_routing() требует совпадения REQUEST_METHOD с ожидаемым (GET/POST).
Статус: ✅ Исправлено.
Где: Все uri() маршруты
Что: /api/v1/devices/id/$id/remove отвечает на GET, POST, DELETE одинаково. Нет REST-семантики.
Статус: ⚠️ Intentional simplification. Сознательное упрощение; возможно будет исправлено позже.
/events/new — timing attackГде: SHServ/Controllers/EventsController.php:17-58
Что: Быстрый flush (200) выполняется только для валидных устройств. Для невалидных — полный стек ошибки. Разница во времени позволяет перебирать device_hard_id.
Статус: ⏸️ Отложено. Требует изменений и на сервере, и в прошивке устройств (авторизация по device_token). Будет решено при переходе к прошивке.
scope_update пишет PHP-файл напрямуюГде: SHServ/Controllers/ScriptsRESTAPIController.php:118
Что: file_put_contents() без атомарности, без бэкапа, без валидации синтаксиса. Прерывание запроса = коррумпированный scope-класс → fatal error.
Статус: ⏸️ Отложено. Будет решено вместе с системой авторизации / RBAC (только админ сможет редактировать scopes).
Где: SHServ/Tools/RateLimiter.php
Что: Счётчик в static array внутри процесса. При 8 FPM workers лимит 60 req/min превращается в 480.
Фикс: Переписан на file-based storage с flock() + fallback на APCu.
Статус: ✅ Исправлено.
Где: SHServ/Routes.php:71-82, SHServ/Controllers/Example_AuthController.php
Что: Все auth-роуты закомментированы, файл называется Example_AuthController.php, но класс AuthController. Аутентификация Vue-клиента происходит вне этих роутов (неясно где).
Где: Все контроллеры
Что: Любой аутентифицированный пользователь может удалять устройства, сканировать сеть, перезаписывать scope-код.
scope_file — disclosure исходного кодаГде: SHServ/Controllers/ScriptsRESTAPIController.php:75-95
Что: Возвращает raw PHP source файла scope. Утечка логики и путей.
update_alias падает при сохранении того же aliasГде: DevicesRESTAPIController.php:359-386, AreasRESTAPIController.php:172-201
Что: alias_is_uniq() не принимает exclude_id. Обновление alias на текущее значение возвращает ошибку "already exists".
Где: new_area, update_name, update_description
Что: Нет maxlength проверок. Можно записать multi-MB строку.
Где: Example_AuthController.php
Что: Нет rate limit, account lockout, progressive delay на входе.
Где: SHServ/Utils.php
Что: API не отдаёт CORS-заголовки. Vue-клиент на другом origin получит CORS errors.
api_auth_guard() вызывает exitГде: SHServ/App.php:110-120
Что: Прерывает PHP-процесс. Shutdown handlers, логирование, cleanup пропускаются.
Где: remove_device, remove_area, reset_device, scope_remove
Что: Нет ?confirm=true, soft-delete или cascade warning.
Где: SHServ/App.php:126-143
Что: file_get_contents('php://input') без Content-Length проверки. 100MB JSON → OOM.
/events/new без rate limitingГде: SHServ/App.php:65-108
Что: check_api_auth() применяет rate limit только к /api/v1/*. /events/new не подпадает.
EventsController передаёт raw $data в handlersГде: SHServ/Controllers/EventsController.php:50-57
Что: Нет схемы валидации данных события. Rogue device может слать malformed payload.
scanning__all — network reconnaissanceГде: SHServ/Controllers/DevicesRESTAPIController.php:20-27
Что: Любой auth-пользователь может сканировать всю подсеть и получить IP/MAC/firmware.
total в areas_list scriptsГде: AreasRESTAPIController.php:260-292
Что: total считает сырые записи, но ответ фильтрует отсутствующие scope-классы.
device_status)Где: DevicesRESTAPIController.php:169-204
Что: Контроллер напрямую меняет connection_status и last_contact у entity. Это работа Model.
validate_positive_int_ids — weak comparisonГде: SHServ/Middleware/Controller.php:26-33
Что: "123.0" != 123 → false, пропускает. Нет strict comparison.
CallControl reflection на каждый запросГде: Fury/Kernel/CallControl.php:148-165
Что: ReflectionClass + ReflectionMethod без кэширования. Лишний overhead.
devices_list не фильтрует по статусуГде: DevicesRESTAPIController.php:296-311
Что: Роут /api/v1/devices/list не имеет $status параметра. Метод всегда возвращает active.
response_error всегда 400Где: SHServ/Utils.php:14-23
Что: device_not_found должен быть 404, alias_already_exists — 409.
reboot_devices игнорирует частичные паденияГде: AreasRESTAPIController.php:209-227
Что: Нет rollback, нет separate failed_count.
ControlScripts конструктор делает reflection для путиГде: SHServ/Middleware/ControlScripts.php:24
Что: Можно заменить на __FILE__.
Где: Fury/Kernel/CallControl.php:150
Что: Если сработает multi-match баг, повторный вызов того же контроллера увидит грязное состояние.
Где: Все контроллеры
Что: Нет persistent лога API-вызовов. Невозможно расследование.
Дата: 2026-06-02
Полный отчёт: server/docs/review-phase-4-automation-scripts-infrastructure.md
Где: SHServ/Middleware/ControlScripts.php:12-13
Что: static $regular_scripts и static $actions_scripts накапливаются в PHP-FPM worker. Удаление scope-файла не очищает static-память. Тесты сбрасывают reflection'ом, в production — нет.
Фикс: ControlScripts::flush_statics() вызывается в начале App::control_scripts_init(). Статус: ✅ Исправлено.
Где: SHServ/Controllers/ScriptsRESTAPIController.php:118
Что: file_put_contents($filepath, $file) пишет напрямую в live PHP-файл. Прерывание = коррумпированный scope-класс → fatal parse error.
Фикс: Temp file → rename() atomically, keep .bak.
Где: SHServ/Controllers/EventsController.php:30, 48-57
Что: После fastcgi_finish_request() обработчики делают синхронные HTTP-вызовы к устройствам. set_time_limit(10) на весь блок, но 5 dispatches × N device calls может превысить лимит.
Фикс: Strict per-handler timeout, async queue, или non-blocking HTTP.
scope_update (reinforced)Где: SHServ/Controllers/ScriptsRESTAPIController.php:81-102
Что: Любой auth-пользователь перезаписывает scope-файл произвольным PHP. Файл включается на следующем запросе. Нет RBAC.
Фикс: Sandbox DSL, php -l, disable live editing в production, admin role.
sync-map.json loaded redundantly, no cachingГде: ControlScripts/Common.php:8-22
Что: Каждый scope re-reads sync-map.json через file_get_contents. С 4 scope'ами — 4 чтения за запрос (~9 KB каждое).
Фикс: Загрузить один раз в App::control_scripts_init() и раздать scopes.
Где: SHServ/Controllers/CronController.php:11-22
Что: run_regular_cron_scripts() вызывает $script["script"]() без try/catch. Один exception = остановка цикла, остальные скрипты не выполняются.
Фикс: Wrap each в try/catch, log, continue.
Где: SHServ/Middleware/ControlScripts.php:120-141
Что: run_action_script() вызывает closure без try/catch. Exception убивает API-запрос.
Фикс: Wrap в try/catch, return structured error.
get_source_code() re-reads file on every registrationГде: SHServ/Middleware/ControlScripts.php:143-160
Что: file() + array_slice при каждом add_action_script. 20 скриптов = 20 чтений файла.
Фикс: Cache file contents per class.
Где: SHServ/App.php:145-161
Что: control_scripts_init() сканирует и инстанциирует ВСЕ .php в Scopes/ на каждый запрос. Даже disabled scopes выполняют конструктор и 4 register_* метода.
Фикс: Lazy-load или cache metadata.
Где: SHServ/Controllers/CronController.php
Что: run_regular_cron_scripts() и status_update_scanning() return nothing. Невозможно узнать, выполнились ли скрипты, сколько упало.
Фикс: Return structured JSON с execution summary.
Где: Fury/Kernel/Logging.php:79-119
Что: Logs/d.m.Y.log.json растёт бесконечно. Нет max size, rotation, cleanup.
Фикс: Rotate by size, archive, или switch to line-based (NDJSON).
Где: Fury/Kernel/Logging.php:49-69
Что: set($place, $title, $message) — только три строки. Нет severity, request ID, structured fields.
Фикс: Add severity, correlation ID, timestamp per entry, context array.
Где: Fury/Modules/ErrorHandler/ErrorHandler.php:127-147
Что: Если FCONF["debug"] = true, рендерит file paths, source code, stack traces. .env отсутствует → fallback debug=false, но misconfiguration раскрывает internals.
Фикс: Production = generic JSON error, never HTML.
Где: project-wide
Что: Нет .sql файлов, install script, schema versioning. scripts table definition только в тестах (ScriptsModelStateTest.php).
Фикс: server/migrations/ с numbered SQL и CLI runner.
.env absent — insecure defaultsГде: SHServ/config.php:22-28
Что: Fallback DB credentials: user => "root", password => "".
Фикс: Fail hard if .env missing in production.
Где: SHServ/Middleware/ControlScripts.php:24-26
Что: Для каждого scope конструктор делает new Scripts() -> script_state() = DB round-trip. N scopes = N+1 запросов.
Фикс: Batch-fetch all scope states в одном query.
sync_map() returns mutable shared referenceГде: SHServ/Middleware/ControlScripts.php:166-168
Что: Возвращает self::$sync_map_storage. Scope может мутировать глобальную карту, влияя на другие scopes.
Фикс: Return deep copy, или immutable storage.
DeviceScanner silently discards failuresГде: SHServ/Tools/DeviceScanner.php:144-146
Что: Failed IPs skipped без логирования. Нет audit trail для disappeared devices.
Фикс: Log scan results: found, lost, unreachable.
text-msgs.php empty strings for SHServ errorsГде: SHServ/text-msgs.php:30-48
Что: device_not_found, unknown_device, error_of_device_auth, db_error, action_script_not_found, scope_not_found и др. имеют пустые значения. Клиент получает пустые msg.
Фикс: Заполнить meaningful messages.
RequiredControlScriptsScope mixes business logic with event handlingГде: SHServ/RequiredControlScriptsScope.php:12-15
Что: online handler напрямую мутирует $device->device_ip, $device->connection_status, $device->update(). Это business logic, не event handling.
Фикс: Перенести в Devices model или dedicated service.
EventsHandlers devmode clutterГде: SHServ/EventsHandlers.php:21-60
Что: Devmode handlers регистрируются в коде всегда, проверка runtime.
Фикс: Conditional class loading или separate dev bootstrap.
console.php only one commandГде: server/console.php:8-19
Что: Только get.config. Нет миграций, health check, log cleanup, manual cron run.
Фикс: Command registry с useful ops.
sync-map.json no schema validationГде: ControlScripts/Common.php:8-22
Что: Raw JSON без schema check. Нет проверки alias existence, channel validity, cycle detection.
Фикс: JSON Schema + validation at bootstrap.
ControlScriptsInterface lacks return typesГде: SHServ/Implements/ControlScriptsInterface.php:13-32
Что: Interface methods без return type declarations, implementations используют : void.
Фикс: Add void to interface.
Где: project-wide
Что: Нет counters: events dispatched, actions executed, cron failed, device API latency, handler latency.
Фикс: Lightweight in-memory metrics endpoint или Prometheus integration.
add_event_handler discards return valuesГде: SHServ/Middleware/ControlScripts.php:34-38
Что: Wrapper closure игнорирует return value handler'а. Нет stopPropagation.
Фикс: Return handler result, или explicit propagation control.
spotlights_by_timeГде: ControlScripts/Scopes/SpotlightsScope.php:64-70
Что: Registered but empty body. Pollutes cron loop.
Фикс: Remove или implement.
TestScriptsScope hardcodes production deviceГде: ControlScripts/Scopes/TestScriptsScope.php:23-26
Что: Alias test_stand_relay hardcoded. В production может не существовать.
Фикс: Gate behind devmode или separate test directory.
EventsModel inconsistent namingГде: SHServ/Models/EventsModel.php
Что: global_any_device_event_call, global_device_event_call, channel_device_event_call — inconsistent naming pattern.
Фикс: Standardize to {scope}_{target}_event_call.
ControlScripts constructor fragile string parsingГде: SHServ/Middleware/ControlScripts.php:24
Что: explode("\\", str_replace("\\Scopes", "", str_replace("SHServ", "", static::class))) — хрупко.
Фикс: basename(str_replace('\\', '/', static::class)).
Scripts::remove_scope TOCTOU raceГде: SHServ/Models/Scripts.php:29-51
Что: file_exists() then unlink(). File may change between calls.
Фикс: Just unlink() and check result.
Где: SHServ/Controllers/EventsController.php
Что: Нет tracing ID correlating original event with downstream device API calls.
Фикс: Generate correlation_id and propagate through handlers.
Logging JSON not grep-friendlyГде: Fury/Kernel/Logging.php:79-119
Что: Entire JSON file loaded to parse/search.
Фикс: NDJSON (newline-delimited JSON) for append-only, grep-friendly format.
RequiredControlScriptsScope mixed with optional scopesГде: SHServ/App.php:145-161
Что: Required scope регистрируется в те же static arrays. Если statics cleared — required handlers lost.
Фикс: Separate storage for required vs optional.
error_handler config ignores notices/deprecationsГде: SHServ/config.php:39-41
Что: important_errors only E_WARNING, E_ERROR, E_CORE_ERROR, EXCEPTION. Notices и deprecations silently ignored.
Фикс: Include в dev; log (не display) в production.
| Файл | Phase | Проблемы | Статус |
|---|---|---|---|
SHServ/config.php |
1.3 | Secrets, debug mode | ✅ .env + загрузка |
SHServ/App.php |
1.1, 2.3, 5.1, 5.3, 5.9 | Auth middleware, сет-эффект, is_dir, root_folder | ✅ Все исправлены |
SHServ/Routes.php |
1.1, 3.5 | Нет auth, нет rate limit | ✅ Guard + RateLimiter в App.php |
SHServ/Sessions.php |
1.5, 1.6 | Токены, cookie flags | ✅ random_bytes(32), Secure/HttpOnly/Strict |
SHServ/Models/Example_Auth.php |
1.4 | SHA1 | ✅ Argon2id + fallback |
SHServ/Controllers/ScriptsRESTAPIController.php |
3.1, 3.2, 3.3, 3.4, 5.4 | Валидация, raw ответ, path traversal, alias | ✅ Все исправлены |
SHServ/Controllers/DevicesRESTAPIController.php |
3.1, 4.5 | IP валидация, reset ignore response | ✅ Все исправлены |
SHServ/Controllers/AreasRESTAPIController.php |
3.1 | Валидация area | ✅ alias regex |
SHServ/Controllers/EventsController.php |
3.1 | Валидация event | 🟡 Не затронут |
SHServ/Models/Devices.php |
2.3 | Нет транзакции | ✅ Транзакция добавлена |
SHServ/Models/Areas.php |
5.10 | Magic number, пустой stub | ✅ Константа MAX_LIMIT, stub удалён |
SHServ/Models/Scripts.php |
1.7 | unlink без auth | 🟡 Не затронут (обход через auth guard) |
SHServ/Tools/DeviceScanner.php |
4.1 | 253 concurrent curl | ✅ Batch size 32 |
SHServ/Tools/DeviceAPI/Base.php |
4.2, 4.3 | Нет retry, hardcoded timeouts | ✅ 3 retries + backoff + env таймауты |
SHServ/Tools/DeviceAPI/Hatch.php |
5.6 | Undefined variable | ✅ $state вместо $status_response |
Fury/Modules/ThinBuilder/ThinBuilder.php |
1.2, 2.4 | SQL injection, silent null | ✅ Prepared statements + exception |
Fury/Modules/ThinBuilder/ThinBuilderProcessing.php |
1.2 | SQL injection | ✅ validate_identifier + ? placeholders |
Fury/Modules/ErrorHandler/ErrorHandler.php |
2.1, 2.2 | Инвертированная логика, закомментированный handler | ✅ Исправлено |
Fury/Kernel/Logging.php |
5.11 | Логи в web root | ✅ flock, 0640, путь вне web root |
console.php |
1.7 | Слив конфига | ✅ unset db.password/user |
SHServ/Entities/User.php |
5.5 | new Profile($uid) баг |
✅ Запрос profile_id из БД |
ControlScripts/Common.php |
5.8 | Hardcoded alias'ы | ✅ Вынесено в sync-map.json |
SHServ/.env.example |
1.3 | — | ✅ Добавлен |
server/composer.json |
Тесты | — | ✅ PHPUnit 10 |
server/tests/ |
Тесты | — | ✅ ThinBuilder, RateLimiter, PasswordHash, Entity, Area, Sessions, Utils |