Newer
Older
smart-home-server / docs / server-audit.md

Аудит сервера SHServ

Дата: 2026-06-03
Версия PHP: 8.x
Файлов: 96 .php
Фреймворк: Fury (кастомный MVC)


Как читать этот документ

Каждая фаза — законченный пакет работ. Внутри фазы задачи отсортированы по приоритету (критичные → высокие → средние → низкие). Фазы следуют друг за другом: нет смысла начинать Phase 3, пока не закрыта Phase 1.

Приоритеты:

  • 🔴 Критично — угроза безопасности, возможен инцидент
  • 🟠 Высоко — операционный риск, падение стабильности или утечка данных
  • 🟡 Средне — технический долг, влияет на поддерживаемость
  • 🟢 Низко — косметика, можно отложить

Phase 1 — Безопасность (Security Foundation) ✅ Выполнена

Цель: Закрыть векторы, через которые злоумышленник может получить полный доступ к системе.

Коммит: 175224e (ветка dev)

Блокер для следующих фаз: нет смысла строить валидацию и обработку ошибок поверх дыр в аутентификации и SQL.

1.1 🔴 Аутентификация на REST API

Где: server/SHServ/Routes.php:58-82
Что: Все endpoint'ы /api/v1/* открыты без проверки сессии/токена. Любой HTTP-запрос может управлять устройствами, менять скрипты, перезагружать реле.

Фикс:

  1. Добавить middleware-слой в Routes.php (или App.php) перед роутингом.
  2. Проверять Authorization: Bearer <token> или Cookie: auth_token=<token>.
  3. Исключения (публичные): POST /events/new (устройства шлют без сессии, но с device token).
  4. Закрыть dev-роуты (/dev/test/*) если devmode === false.

1.2 🔴 SQL-инъекции в ThinBuilder

Где:

  • server/Fury/Modules/ThinBuilder/ThinBuilderProcessing.php:40-108
  • server/Fury/Modules/ThinBuilder/ThinBuilder.php:58-87

Что: addslashes() используется как основной механизм экранирования. Это недостаточно для MySQL (возможен multibyte-обход). Все insert, update, delete и where собирают raw SQL строки конкатенацией.

Фикс:

  1. Переписать ThinBuilder на PDO prepared statements.
  2. Все пользовательские значения — через ? плейсхолдеры + bindValue().
  3. Идентификаторы (имена таблиц, колонок) через whitelist/квотирование.
  4. Если переписывать весь ThinBuilder слишком дорого — добавить PreparedStatement-wrapper вокруг существующего билдера, постепенно мигрируя модели.

1.3 🔴 Секреты вне версионного контроля

Где: server/SHServ/config.php:6,13-14,20

Что:

  • debug => true и devmode => true в коммите — стектрейсы и dev-tools доступны на проде.
  • db.user = "eugene", db.password = "root" в plaintext в git.

Фикс:

  1. Создать .env файл на уровне server/ (добавить в .gitignore).
  2. Использовать библиотеку типа vlucas/phpdotenv или простой парсер.
  3. config.php оставить как config.example.php с placeholder'ами.
  4. Переименовать живой config.php.env-based загрузку.
  5. По умолчанию debug = false, devmode = false. Переопределять через env.

1.4 🔴 Хеширование паролей

Где: server/SHServ/Models/Example_Auth.php:24

Что: Пароли хешируются sha1(), который криптографически сломан и быстрый к брутфорсу.

Фикс:

  1. password_hash($password, PASSWORD_ARGON2ID) при регистрации/смене.
  2. password_verify($password, $hash) при входе.
  3. Миграция: добавить колонку password_hash, при первом успешном password_verify обновлять из sha1.

1.5 🔴 Криптографически стойкие токены сессий

Где: 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',
]);

1.7 🟠 CLI get.config сливает конфиг

Где: server/console.php:10-12

Что: Команда get.config выводит весь массив FCONF (включая пароль БД) без аутентификации.

Фикс: Удалить команду или закрыть проверкой локального файла-ключа (например, только если php_sapi_name() === 'cli' и uid = владелец файла).


Phase 2 — Целостность данных и обработка ошибок ✅ Выполнена

Цель: Убрать silent failures, сделать транзакции атомарными, починить инвертированную логику error handler'а.

Коммит: 1a30037 (ветка dev)

Блокер: пока error handler инвертирован, трудно доверять логам и репортам.

2.1 🟠 Инвертированный ErrorHandler

Где: server/Fury/Modules/ErrorHandler/ErrorHandler.php:26-29

Что:

if(!FCONF["debug"]) {
    error_reporting(-1);
}

Ошибки показываются, когда debug выключен. Логика наоборот.

Фикс: Поменять ветки — показывать error_reporting(-1) когда debug === true, подавлять (или логировать) когда false.


2.2 🟠 Убрать сет_exception_handler комментарий

Где: server/Fury/Modules/ErrorHandler/ErrorHandler.php:41

Что: set_exception_handler закомментирован — uncaught exceptions утекают со стектрейсами.

Фикс: Раскомментировать и направлять через exception_handler.


2.3 🟠 Транзакция при создании устройства

Где: 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;
}

2.4 🟠 Silent null на SQL-ошибках

Где: server/Fury/Modules/ThinBuilder/ThinBuilder.php:28-30

Что: query() возвращает null при ошибке SQL, не логируя и не бросая исключение. TODO-комментарий подтверждает, что это известный баг.

Фикс:

  1. Включить PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION в DB.php.
  2. Убрать return null — пусть PDO бросает.
  3. Обёртка try/catch в контроллере, маппинг в response_error.

2.5 🟡 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 для серверных.


2.6 🟡 Пустые стабы

Где:

  • server/SHServ/Models/Areas.php:103-105remove_obsolete() пустой
  • server/SHServ/Helpers/Validator.php — класс пустой, не используется

Фикс: Реализовать или удалить.


Phase 3 — Укрепление API ✅ Выполнена

Цель: Валидация входных данных, единообразные ответы, защита от abuse.

Коммит: 35f9ec8 (ветка dev)

Блокер: валидация входных данных бессмысленна, если за ней всё равно стоит уязвимый ThinBuilder (Phase 1). Поэтому Phase 3 идёт после Phase 1.

3.1 🟠 Валидация входных данных

Где:

  • server/SHServ/Controllers/AreasRESTAPIController.php:52-77new_area не валидирует type, alias, display_name
  • server/SHServ/Controllers/DevicesRESTAPIController.php:32 — IP проверяется только strlen < 7
  • server/SHServ/Controllers/ScriptsRESTAPIController.php:19-29run_action_script передаёт сырые $params в callable без валидации
  • server/SHServ/Controllers/DevicesRESTAPIController.php:206do_device_action передаёт сырые $params на устройство

Фикс:

  1. Ввести Validator (или использовать filter_var + regex).
  2. alias/^[a-z0-9_]+$/, display_name — max 255, type — whitelist.
  3. IP — filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4).
  4. $params — whitelist по action alias или JSON Schema.

3.2 🟠 scope_file возвращает не JSON

Где: server/SHServ/Controllers/ScriptsRESTAPIController.php:59-79

Что: На success возвращает raw PHP source code, на error — JSON. Клиент должен угадывать формат.

Фикс: Всегда JSON-обёртка:

{ "status": true, "data": { "source": "<?php ..." } }

3.3 🟠 Scope update — path traversal

Где: server/SHServ/Controllers/ScriptsRESTAPIController.php:81-102

Что: Принимает $path и $file от клиента, пишет на диск с минимальной проверкой (strpos($filepath, ".php")). Можно писать вне ControlScripts/Scopes/.

Фикс:

  1. Разрешить запись только в __DIR__ . "/../../ControlScripts/Scopes/".
  2. Проверять realpath($filepath) начинается с разрешённого префикса.
  3. Использовать whitelist имён файлов или UUID.

3.4 🟠 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);
}

3.5 🟡 Рейт-лимитинг

Где: 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.


Phase 4 — Коммуникация с устройствами ✅ Выполнена

Цель: Сделать работу с устройствами отказоустойчивой и неблокирующей.

Коммит: b4968d4 (ветка dev)

4.1 🟠 DeviceScanner — 253 одновременных cURL

Где: server/SHServ/Tools/DeviceScanner.php:72-98

Что: scan_ips создаёт curl handle для каждого IP диапазона. Для /24 — 253 хендла одновременно. Может исчерпать fd и память.

Фикс: Batch-сканирование, sliding window (например, 32 параллельных хендла максимум). Остальные ставить в очередь.


4.2 🟠 Нет retry / exponential backoff на HTTP к устройствам

Где: server/SHServ/Tools/DeviceAPI/Base.php:138-226

Что: Один transient network failure = полный провал запроса. Нет повторных попыток.

Фикс: Добавить retry loop (3 попытки, backoff 100ms, 300ms). Только для идемпотентных операций (status, action). POST на /setup — без retry или с idempotency key.


4.3 🟠 Хардкод таймаутов на cURL

Где: server/SHServ/Tools/DeviceAPI/Base.php:178-179

Что: CONNECTTIMEOUT => 1, TIMEOUT => 5 — не конфигурируются.

Фикс: Вынести в FCONF['device_api_timeout'] или device-level config.


4.4 🟡 Синхронные blocking-вызовы к устройствам

Где:

  • server/SHServ/Controllers/DevicesRESTAPIController.php:177device_status
  • server/SHServ/RequiredControlScriptsScope.php:12-21online event handler вызывает get_about()

Что: PHP-поток блокируется на время cURL timeout. Если устройство offline, ждём полные 5 секунд.

Фикс:

  1. Для bulk-операций — curl_multi (уже есть в DeviceScanner, переиспользовать).
  2. Для event handler'ов — не блокировать: пометить device как "pending update" и обновить lazily.

4.5 🟡 reset_device игнорирует ответ устройства

Где: server/SHServ/Controllers/DevicesRESTAPIController.php:382

Что: Вызывает $device->device_api()->reset() и не проверяет результат. Если устройство offline, клиент получает "success".

Фикс: Проверять http_code == 200, иначе device_request_fail.


Phase 5 — Качество кода и техдолг ✅ Выполнена

Цель: Убрать мёртвый код, дедупликацию, странные сайд-эффекты.

Коммит: d9c9e17 (ветка dev)

Можно выполнять параллельно с Phase 2–4, но не раньше Phase 1.

5.1 🟡 Сайд-эффект в конструкторе App

Где: server/SHServ/App.php:35-36

Что: На каждый HTTP-запрос принудительно включается spotlights_off action script. Это не должно жить в bootstrap.

Фикс: Вынести в миграцию/seed-скрипт, выполняемый один раз при установке.


5.2 🟡 Дедупликация place_in_area / unassign_from_area

Где:

  • server/SHServ/Controllers/DevicesRESTAPIController.php:238-279
  • server/SHServ/Controllers/AreasRESTAPIController.php:108-129

Что: Логика размещения устройства в area дублируется почти дословно.

Фикс: Вынести в сервис AreaPlacementService или shared validator.


5.3 🟡 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)

5.4 🟡 Переменная $device вместо $script

Где: server/SHServ/Controllers/ScriptsRESTAPIController.php:143,146-148

Что: Переменная называется $device, но содержит Script. На ошибке возвращается alias "device_not_found" вместо "script_not_exists".

Фикс: Переименовать и исправить alias.


5.5 🟡 User.php конструирует Profile с $uid

Где: server/SHServ/Entities/User.php:19

Что: new Profile($uid) — вместо profile_id передаётся uid. TODO-комментарий подтверждает баг.

Фикс: Передать реальный profile_id (запросить из БД или взять из поля profile_id).


5.6 🟡 Hatch.php — undefined variable

Где: server/SHServ/Tools/DeviceAPI/Hatch.php:21,29,37,45

Что: Методы is_opened, is_closed, is_opening, is_closing ссылаются на $status_response, которой нет в scope.

Фикс: Использовать $state или корректное имя переменной.


5.7 🟡 Мёртвый код в Scope'ах

Где:

  • server/ControlScripts/Scopes/OfficeRoomScope.php:71-98
  • server/ControlScripts/Scopes/SpotlightsScope.php:127-160
  • server/ControlScripts/Scopes/TestScriptsScope.php:31-71

Что: Большие закомментированные блоки.

Фикс: Удалить или перенести в docs/notes.md.


5.8 🟢 Hardcoded alias'ы в ControlScripts/Common.php

Где: server/ControlScripts/Common.php:8-123

Что: Десятки device alias'ов (spotlight_main_back_1, buttons_backdoor...) зашиты в код.

Фикс: Вынести sync map в БД или dedicated YAML/JSON конфиг.


5.9 🟢 root_folder() хрупкий

Где: server/SHServ/App.php:63-65

Что:

list($root) = explode('SHServ', __DIR__);

Сломается, если директория переименуется.

Фикс:

return dirname(__DIR__, 2);

5.10 🟢 get_all() magic number

Где: server/SHServ/Models/Areas.php:92

Что: Хардкод limit 1000.

Фикс: Константа AREAS_MAX_LIMIT или pagination.


5.11 🟢 Логи в web-root без защиты

Где: server/Fury/Kernel/Logging.php:88-96

Что: Логи пишутся в SHServ/Logs/ (под web root), chmod 0755, нет flock.

Фикс:

  1. Перенести логи за пределы document root.
  2. chmod 0640.
  3. Использовать 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\Devicesremove_device, reboot_device happy-path.
  • App::api_auth_guard() exit/headers integration (end-to-end).
  • EventsController валидация.
  • DeviceScanner batch scanning.

Phase 6 — Ревью Security, Auth, Device Integration (2026-06-02)

Цель: Повторное ревью слоя аутентификации и интеграции с устройствами после внедрения тестов.

Коммит: — (ветка dev)

Этот ревью выполнен после добавления 113 тестов. Некоторые проблемы обнаружены при анализе кода для тестирования.

6.1 🔴 RateLimiter не работает в PHP-FPM/Apache

Где: server/SHServ/Tools/RateLimiter.php

Что: static $requests — in-memory property процесса. При PHP-FPM каждый запуск — новый процесс, счётчик сбрасывается. Rate limiting фактически отсутствует.

Фикс: Перенести на Redis / memcached / DB, или file-based storage с flock().


6.2 🔴 /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/.


6.3 🔴 Race condition в 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) в тестовую схему. Статус:Исправлено.


6.4 🔴 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) перед инъекцией. Статус:Исправлено.


6.5 🟠 Post-commit remote state inconsistency

Где: 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.


6.6 🟠 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.

Фикс: Сбрасывать $device->device_auth_instance = null в DeviceAuth::kill().


6.7 🟠 Replay-атаки на /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) и/или подпись полезной нагрузки.


6.8 🟡 generate_token(16) — низкая энтропия для device_token

Где: server/SHServ/Utils.php:144-149

Что: 16 hex-символов = 64 бита. Для локальной сети терпимо, но на грани.

Фикс: Поднять до 32 (128 бит) для device_token.


6.9 🟡 Нет валидации $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) в конструкторе.


6.10 🟡 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 при ошибке.


6.11 🟡 Sessions::get_current_session() — лишний UPDATE

Где: server/SHServ/Sessions.php:82-96

Что: Обновление last_using_at при каждом запросе создаёт write load. При масштабе заметно.

Фикс: Обновлять раз в N минут (например, если delta > 60s).


6.12 🟡 EventsControllerset_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).


6.13 🟢 Bearer token parsing — edge case

Где: server/SHServ/App.php:87-89

Что: substr($auth_header, 7) без проверки длины. Header "Bearer" (без пробела) даст пустой token, который пройдёт в !$token → 401. Не критично.

Фикс: Использовать str_starts_with + explode(' ', $auth_header, 2)[1] ?? null.


Phase 7 — Ревью Models и Data Layer (2026-06-02)

Цель: Проверить модели, Entity-классы, ORM ThinBuilder, фабрики и хелперы на корректность, согласованность и производительность.

Ветка: dev

Продолжение ревью после Phase 6. В этой фазе найдено 34 находки: 5 критических, 14 высокого приоритета, 15 среднего/информационного.

7.1 🔴 Entity::select_from_db() падает на отсутствующей строке

Где: 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() и бросать исключение RecordNotFound, если строка отсутствует.


7.2 🔴 DeviceAuth::kill() упадёт в production из-за отсутствия update_at

Где: 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).


7.3 🔴 Device::remove() не инвалидирует токен авторизации

Где: server/SHServ/Entities/Device.php:36-39

Что: Метод меняет status устройства на "removed", но не трогает таблицу device_auth. Строка авторизации остаётся со статусом active, и токен продолжает считаться валидным.

Фикс: В remove() дополнительно вызывать $this->auth()->kill() или делать операцию транзакционной. Статус: ⏸️ Отложено. Будет решено вместе с системой авторизации / RBAC (только админ может удалять устройства).


7.4 🔴 Area::remove() выполняет 3 обновления без транзакции

Где: server/SHServ/Entities/Area.php:267-282

Что: update() на devices (area_id = 0), update() на areas (parent_id = 0), и remove_entity() — три независимых запроса без beginTransaction. При падении после первого или второго остаются битые ссылки.

Фикс: Обернута в beginTransaction/commit/rollBack. Статус:Исправлено.


7.5 🔴 Scripts::select_scripts_by_aliases_types() падает на пустом массиве

Где: server/SHServ/Models/Scripts.php:63-73

Что: При $uniq_names = [] генерируется ... WHERE type = ? AND uniq_name IN () — синтаксическая ошибка SQL.

Фикс: Ранний возврат [] если массив пуст.


7.6 🟠 ThinBuilder не валидирует операторы WHERE

Где: server/Fury/Modules/ThinBuilder/ThinBuilderProcessing.php:147-156

Что: Любая строка проходит как оператор прямо в SQL. Значения параметризованы, но оператор — сырой текст.

Фикс: Whitelist допустимых операторов: =, !=, <>, <, >, <=, >=, LIKE, IN, IS, NOT IN, BETWEEN.


7.7 🟠 validate_identifier() разрешает числовой старт имени

Где: server/Fury/Modules/ThinBuilder/ThinBuilderProcessing.php:62-67

Что: Регулярка /^[a-zA-Z0-9_]+$/ пропускает 123abc, что недопустимо в SQL. Правильная: /^[a-zA-Z_][a-zA-Z0-9_]*$/.


7.8 🟠 escape_string_in_arr() — мёртвый код с опасным экранированием

Где: server/Fury/Modules/ThinBuilder/ThinBuilderProcessing.php:69-79

Что: Метод использует addslashes() вместо prepared statements. Нигде не вызывается, но его наличие создаёт риск.

Фикс: Удалить метод.


7.9 🟠 MetaManager::create_or_update() — race condition

Где: server/SHServ/Models/MetaManager.php:75-86

Что: SELECT, затем INSERT или UPDATE. При конкурентных запросах возможен дубль.

Фикс: INSERT ... ON DUPLICATE KEY UPDATE (MySQL) или UNIQUE-индекс (assignment, ent_id, name).


7.10 🟠 User::__construct() жадно загружает профиль

Где: server/SHServ/Entities/User.php:16-28

Что: Каждое создание User инициирует дополнительный SELECT из profiles. N+1 при массовой выборке.

Фикс: Ленивая загрузка через get_pet_instance.


7.11 🟠 Scripts::remove_scope() — файловая система и БД рассинхронизируются

Где: server/SHServ/Models/Scripts.php:29-51

Что: unlink() выполняется до $script->remove(). Если DB delete упадёт, файл уже удалён.

Фикс: Удалять файл после успешного DB delete, либо оборачивать в транзакцию.


7.12 🟠 Entity::update() всегда возвращает true

Где: server/SHServ/Middleware/Entity.php:67-79

Что: ThinBuilder::update() возвращает rowCount(), но Entity игнорирует его. Если UPDATE не затронул строк, всё равно true.

Фикс: Проверять rowCount() > 0.


7.13 🟠 IN-клауза с не-массивом падает на PHP 8+

Где: server/Fury/Modules/ThinBuilder/ThinBuilderProcessing.php:149-151

Что: count("string") в PHP 8 бросает TypeError. Если по ошибке передать скаляр в IN, приложение упадёт.

Фикс: Проверка is_array($w_item[2]).


7.14 🟠 Короткий синтаксис where не поддерживает IN

Где: server/Fury/Modules/ThinBuilder/ThinBuilderProcessing.php:115-130

Что: ['field', ['v1', 'v2']] превращается в ['field', '=', ['v1', 'v2']] из-за count === 2. Массив пытается забиндиться как скаляр.

Фикс: Обрабатывать массив второго элемента как IN.


7.15 🟠 Entity::update() не сбрасывает modified_fields при исключении

Где: server/SHServ/Middleware/Entity.php:67-79

Что: При PDOException modified_fields остаётся заполненным. Повторный update() запишет устаревшие + новые изменения.

Фикс: try/finally — сбрасывать только при успехе.


7.16 🟠 Device::set_device_token() делает избыточные вызовы auth()

Где: server/SHServ/Entities/Device.php:94-102

Что: auth() вызывается трижды внутри одного метода.

Фикс: Закэшировать $auth = $this->auth().


7.17 🟠 Scripts::set_script_state() чрезвычайно неэффективен

Где: server/SHServ/Models/Scripts.php:75-128

Что: Reflection на каждый вызов для scopes. Для regular/action — O(n·m) линейный поиск.

Фикс: Хеш-таблица keyed by uniq_name.


7.18 🟡 Area::get_exists_types() сканирует всю таблицу вместо DISTINCT

Где: server/SHServ/Models/Areas.php:36-57

Что: Запрашивает все строки, затем дедуплицирует в PHP.

Фикс: SELECT DISTINCT type FROM areas.


7.19 🟡 Area::get_recursive_inner_areas_ids() жёстко ограничен глубиной 10

Где: server/SHServ/Entities/Area.php:129-158

Что: if($lvl >= 10) { return []; } — молча отрезает хвост при глубоком дереве.

Фикс: Исключение при превышении.


7.20 🟡 ThinBuilder::table_fields() ломается на типах без длины

Где: server/Fury/Modules/ThinBuilder/ThinBuilder.php:197-213

Что: explode('(', $raw_field[1]) для INT, TEXT, DATETIME возвращает массив из одного элемента. list() присвоит null.


7.21 🟡 ThinBuilder::tables() только для MySQL

Где: server/Fury/Modules/ThinBuilder/ThinBuilder.php:215-222

Что: SHOW TABLES — MySQL-специфичный синтаксис. При SQLite не работает.


7.22 🟡 Несоответствие тестовой и production схемы device_auth

Где: server/tests/DevicesModelTransactionTest.php:44-52

Что: Тесты создают device_auth с update_at TEXT, но production schema может отличаться. Нет единого источника правды для схемы.

Фикс: Создать schema.sql в репозитории.


7.23 🟡 Нет DB-уровневых UNIQUE-индексов

Где: Все модели

Что: Уникальность проверяется через count() на уровне приложения. Race conditions возможны.

Фикс: Добавить UNIQUE INDEX на areas.alias, devices.alias, devices.device_hard_id (active), users.alias/email, meta.(assignment, ent_id, name).


7.24 🟡 Area и AreaPlacing trait дублируют place_in_area_id

Где: server/SHServ/Entities/Area.php:68-71, server/SHServ/Entities/Traits/AreaPlacing.php:12-15

Что: Идентичные методы. Класс побеждает trait, но дублирование затрудняет поддержку.


7.25 🟡 Creator::create_meta() подделывает update_at

Где: server/SHServ/Factory/Creator.php:32-50

Что: Вставляет фиктивное update_at только чтобы удовлетворить Entity::update().


7.26 🟡 Factory\Getter::get_profile_by() тянет только id

Где: server/SHServ/Factory/Getter.php:25-39

Что: Запрашивает только id, затем создаёт Profile без данных, форсируя ленивую загрузку.


7.27 🟡 Model-конструктор вызывает devtools на каждую инстанциацию

Где: server/SHServ/Middleware/Model.php:8-13

Что: using_model() — лишний вызов в production.


7.28 🟡 Area::remove() присваивает строку "0" вместо integer

Где: server/SHServ/Models/Areas.php:20

Что: "parent_id" => "0" — string вместо int.


7.29 🟡 MetaWrap::$meta_manager_instance — статический mutable singleton

Где: server/SHServ/Helpers/MetaWrap.php:9

Что: Глобальное mutable состояние.


7.30 🟡 ThinBuilder::query() — raw SQL без параметризации

Где: server/Fury/Modules/ThinBuilder/ThinBuilder.php:17-42

Что: Метод принимает строку SQL и выполняет напрямую. Опасен при использовании с пользовательским input.


7.31 🟡 Scripts::get_scripts_list() — O(n·m) вместо хеш-таблицы

Где: server/SHServ/Models/Scripts.php:170-184

Что: array_filter внутри цикла. При 50 скриптах — 2500 сравнений.

Фикс: Построить array_reduce → keyed lookup.


7.32 🟡 Несогласованные типы возврата ошибок в Models

Где: Models/Areas.php, Models/Devices.php

Что: Areas::create_new_area()Area|null, Devices::connect_new_device()Device|Array.

Фикс: Единый Result-объект или Exception-based flow.


7.33 🟡 Area::alias_is_uniq() не исключает саму себя при обновлении

Где: server/SHServ/Models/Areas.php:27-34

Что: При использовании для update собственный alias будет засчитан как дубль.


7.34 🟡 ThinBuilder flat where syntax в Entity::remove_entity()

Где: server/SHServ/Middleware/Entity.php:85-87

Что: remove_entity() использует плоский массив ["id", "=", $this->id()] вместо nested. Работает, но не единообразно.


Phase 8 — Controllers, Routing, Validation (Phase 3 Review) 📝 Завершена

Дата: 2026-06-02
Полный отчёт: server/docs/review-phase-3-controllers-routing-validation.md

8.1 🔴 Router вызывает ВСЕ совпадающие маршруты

Где: Fury/Modules/Router/RouterImplementation.php
Что: URI_routing() и GET_and_POST_routing() не делают break после первого совпадения. Один HTTP-запрос может вызвать несколько контроллеров.
Статус: ⚠️ By design / intentional. Автор подтвердил, что это ожидаемое поведение фреймворка.

8.2 🔴 Cron endpoints открыты без авторизации

Где: SHServ/Routes.php:61-62
Что: /cron/regular-scripts и /cron/status-update-scanning — публичные URI-роуты. Любой может триггерить сканирование сети и выполнение регулярных скриптов.
Фикс: CronController::ensure_localhost_only() — только 127.0.0.1 / ::1.
Статус:Исправлено.

8.3 🔴 POST-роуты отвечают на GET (method spoofing)

Где: Fury/Modules/Router/RouterImplementation.php:61-91
Что: Роутер проверяет только наличие параметров в $_GET/$_POST, но не $_SERVER['REQUEST_METHOD']. Все деструктивные POST-эндпоинты доступны через GET-ссылку.
Фикс: GET_and_POST_routing() требует совпадения REQUEST_METHOD с ожидаемым (GET/POST).
Статус:Исправлено.

8.4 🔴 URI-роуты игнорируют HTTP метод

Где: Все uri() маршруты
Что: /api/v1/devices/id/$id/remove отвечает на GET, POST, DELETE одинаково. Нет REST-семантики.
Статус: ⚠️ Intentional simplification. Сознательное упрощение; возможно будет исправлено позже.

8.5 🔴 /events/new — timing attack

Где: SHServ/Controllers/EventsController.php:17-58
Что: Быстрый flush (200) выполняется только для валидных устройств. Для невалидных — полный стек ошибки. Разница во времени позволяет перебирать device_hard_id.
Статус: ⏸️ Отложено. Требует изменений и на сервере, и в прошивке устройств (авторизация по device_token). Будет решено при переходе к прошивке.

8.6 🔴 scope_update пишет PHP-файл напрямую

Где: SHServ/Controllers/ScriptsRESTAPIController.php:118
Что: file_put_contents() без атомарности, без бэкапа, без валидации синтаксиса. Прерывание запроса = коррумпированный scope-класс → fatal error.
Статус: ⏸️ Отложено. Будет решено вместе с системой авторизации / RBAC (только админ сможет редактировать scopes).

8.7 🔴 RateLimiter in-memory (bypass через FPM workers)

Где: SHServ/Tools/RateLimiter.php
Что: Счётчик в static array внутри процесса. При 8 FPM workers лимит 60 req/min превращается в 480.
Фикс: Переписан на file-based storage с flock() + fallback на APCu.
Статус:Исправлено.

8.8 🟠 Auth routes — мёртвый код

Где: SHServ/Routes.php:71-82, SHServ/Controllers/Example_AuthController.php
Что: Все auth-роуты закомментированы, файл называется Example_AuthController.php, но класс AuthController. Аутентификация Vue-клиента происходит вне этих роутов (неясно где).

8.9 🟠 Нет RBAC

Где: Все контроллеры
Что: Любой аутентифицированный пользователь может удалять устройства, сканировать сеть, перезаписывать scope-код.

8.10 🟠 scope_file — disclosure исходного кода

Где: SHServ/Controllers/ScriptsRESTAPIController.php:75-95
Что: Возвращает raw PHP source файла scope. Утечка логики и путей.

8.11 🟠 update_alias падает при сохранении того же alias

Где: DevicesRESTAPIController.php:359-386, AreasRESTAPIController.php:172-201
Что: alias_is_uniq() не принимает exclude_id. Обновление alias на текущее значение возвращает ошибку "already exists".

8.12 🟠 Нет лимитов длины текстовых полей

Где: new_area, update_name, update_description
Что: Нет maxlength проверок. Можно записать multi-MB строку.

8.13 🟠 Нет brute-force защиты на auth

Где: Example_AuthController.php
Что: Нет rate limit, account lockout, progressive delay на входе.

8.14 🟠 Нет CORS

Где: SHServ/Utils.php
Что: API не отдаёт CORS-заголовки. Vue-клиент на другом origin получит CORS errors.

8.15 🟠 api_auth_guard() вызывает exit

Где: SHServ/App.php:110-120
Что: Прерывает PHP-процесс. Shutdown handlers, логирование, cleanup пропускаются.

8.16 🟠 Деструктивные операции без подтверждения

Где: remove_device, remove_area, reset_device, scope_remove
Что: Нет ?confirm=true, soft-delete или cascade warning.

8.17 🟠 Неограниченный размер JSON body

Где: SHServ/App.php:126-143
Что: file_get_contents('php://input') без Content-Length проверки. 100MB JSON → OOM.

8.18 🟠 /events/new без rate limiting

Где: SHServ/App.php:65-108
Что: check_api_auth() применяет rate limit только к /api/v1/*. /events/new не подпадает.

8.19 🟠 EventsController передаёт raw $data в handlers

Где: SHServ/Controllers/EventsController.php:50-57
Что: Нет схемы валидации данных события. Rogue device может слать malformed payload.

8.20 🟠 scanning__all — network reconnaissance

Где: SHServ/Controllers/DevicesRESTAPIController.php:20-27
Что: Любой auth-пользователь может сканировать всю подсеть и получить IP/MAC/firmware.

8.21 🟡 Несоответствие total в areas_list scripts

Где: AreasRESTAPIController.php:260-292
Что: total считает сырые записи, но ответ фильтрует отсутствующие scope-классы.

8.22 🟡 Бизнес-логика в контроллере (device_status)

Где: DevicesRESTAPIController.php:169-204
Что: Контроллер напрямую меняет connection_status и last_contact у entity. Это работа Model.

8.23 🟡 validate_positive_int_ids — weak comparison

Где: SHServ/Middleware/Controller.php:26-33
Что: "123.0" != 123false, пропускает. Нет strict comparison.

8.24 🟡 CallControl reflection на каждый запрос

Где: Fury/Kernel/CallControl.php:148-165
Что: ReflectionClass + ReflectionMethod без кэширования. Лишний overhead.

8.25 🟡 devices_list не фильтрует по статусу

Где: DevicesRESTAPIController.php:296-311
Что: Роут /api/v1/devices/list не имеет $status параметра. Метод всегда возвращает active.

8.26 🟡 response_error всегда 400

Где: SHServ/Utils.php:14-23
Что: device_not_found должен быть 404, alias_already_exists — 409.

8.27 🟡 reboot_devices игнорирует частичные падения

Где: AreasRESTAPIController.php:209-227
Что: Нет rollback, нет separate failed_count.

8.28 🟡 ControlScripts конструктор делает reflection для пути

Где: SHServ/Middleware/ControlScripts.php:24
Что: Можно заменить на __FILE__.

8.29 🟡 Singleton-контроллеры держат состояние

Где: Fury/Kernel/CallControl.php:150
Что: Если сработает multi-match баг, повторный вызов того же контроллера увидит грязное состояние.

8.30 🟡 Нет audit log

Где: Все контроллеры
Что: Нет persistent лога API-вызовов. Невозможно расследование.


Phase 9 — Automation, Scripts, Infrastructure (Phase 4 Review) 📝 Завершена

Дата: 2026-06-02
Полный отчёт: server/docs/review-phase-4-automation-scripts-infrastructure.md

9.1 🔴 Static script registries leak state across requests

Где: 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(). Статус:Исправлено.


9.2 🔴 Scope update — non-atomic file write

Где: SHServ/Controllers/ScriptsRESTAPIController.php:118
Что: file_put_contents($filepath, $file) пишет напрямую в live PHP-файл. Прерывание = коррумпированный scope-класс → fatal parse error.
Фикс: Temp file → rename() atomically, keep .bak.


9.3 🔴 Events post-flush handlers can hang indefinitely

Где: 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.


9.4 🔴 RCE via scope_update (reinforced)

Где: SHServ/Controllers/ScriptsRESTAPIController.php:81-102
Что: Любой auth-пользователь перезаписывает scope-файл произвольным PHP. Файл включается на следующем запросе. Нет RBAC.
Фикс: Sandbox DSL, php -l, disable live editing в production, admin role.


9.5 🔴 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.


9.6 🔴 Cron scripts stop on first failure

Где: SHServ/Controllers/CronController.php:11-22
Что: run_regular_cron_scripts() вызывает $script["script"]() без try/catch. Один exception = остановка цикла, остальные скрипты не выполняются.
Фикс: Wrap each в try/catch, log, continue.


9.7 🟠 Action scripts without error handling

Где: SHServ/Middleware/ControlScripts.php:120-141
Что: run_action_script() вызывает closure без try/catch. Exception убивает API-запрос.
Фикс: Wrap в try/catch, return structured error.


9.8 🟠 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.


9.9 🟠 Eager scope instantiation — no lazy loading

Где: SHServ/App.php:145-161
Что: control_scripts_init() сканирует и инстанциирует ВСЕ .php в Scopes/ на каждый запрос. Даже disabled scopes выполняют конструктор и 4 register_* метода.
Фикс: Lazy-load или cache metadata.


9.10 🟠 Cron endpoints return empty response

Где: SHServ/Controllers/CronController.php
Что: run_regular_cron_scripts() и status_update_scanning() return nothing. Невозможно узнать, выполнились ли скрипты, сколько упало.
Фикс: Return structured JSON с execution summary.


9.11 🟠 Logging writes to JSON without rotation

Где: 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).


9.12 🟠 No structured logging / severity / correlation IDs

Где: Fury/Kernel/Logging.php:49-69
Что: set($place, $title, $message) — только три строки. Нет severity, request ID, structured fields.
Фикс: Add severity, correlation ID, timestamp per entry, context array.


9.13 🟠 ErrorHandler may leak debug info

Где: 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.


9.14 🟠 No DB schema / migrations

Где: project-wide
Что: Нет .sql файлов, install script, schema versioning. scripts table definition только в тестах (ScriptsModelStateTest.php).
Фикс: server/migrations/ с numbered SQL и CLI runner.


9.15 🟠 .env absent — insecure defaults

Где: SHServ/config.php:22-28
Что: Fallback DB credentials: user => "root", password => "".
Фикс: Fail hard if .env missing in production.


9.16 🟠 ControlScripts constructor — DB check per scope (N+1)

Где: SHServ/Middleware/ControlScripts.php:24-26
Что: Для каждого scope конструктор делает new Scripts() -> script_state() = DB round-trip. N scopes = N+1 запросов.
Фикс: Batch-fetch all scope states в одном query.


9.17 🟠 sync_map() returns mutable shared reference

Где: SHServ/Middleware/ControlScripts.php:166-168
Что: Возвращает self::$sync_map_storage. Scope может мутировать глобальную карту, влияя на другие scopes.
Фикс: Return deep copy, или immutable storage.


9.18 🟠 DeviceScanner silently discards failures

Где: SHServ/Tools/DeviceScanner.php:144-146
Что: Failed IPs skipped без логирования. Нет audit trail для disappeared devices.
Фикс: Log scan results: found, lost, unreachable.


9.19 🟠 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.


9.20 🟠 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.


9.21 🟡 EventsHandlers devmode clutter

Где: SHServ/EventsHandlers.php:21-60
Что: Devmode handlers регистрируются в коде всегда, проверка runtime.
Фикс: Conditional class loading или separate dev bootstrap.


9.22 🟡 console.php only one command

Где: server/console.php:8-19
Что: Только get.config. Нет миграций, health check, log cleanup, manual cron run.
Фикс: Command registry с useful ops.


9.23 🟡 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.


9.24 🟡 ControlScriptsInterface lacks return types

Где: SHServ/Implements/ControlScriptsInterface.php:13-32
Что: Interface methods без return type declarations, implementations используют : void.
Фикс: Add void to interface.


9.25 🟡 No observability metrics

Где: project-wide
Что: Нет counters: events dispatched, actions executed, cron failed, device API latency, handler latency.
Фикс: Lightweight in-memory metrics endpoint или Prometheus integration.


9.26 🟡 add_event_handler discards return values

Где: SHServ/Middleware/ControlScripts.php:34-38
Что: Wrapper closure игнорирует return value handler'а. Нет stopPropagation.
Фикс: Return handler result, или explicit propagation control.


9.27 🟡 Empty regular script spotlights_by_time

Где: ControlScripts/Scopes/SpotlightsScope.php:64-70
Что: Registered but empty body. Pollutes cron loop.
Фикс: Remove или implement.


9.28 🟡 TestScriptsScope hardcodes production device

Где: ControlScripts/Scopes/TestScriptsScope.php:23-26
Что: Alias test_stand_relay hardcoded. В production может не существовать.
Фикс: Gate behind devmode или separate test directory.


9.29 🟡 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.


9.30 🟡 ControlScripts constructor fragile string parsing

Где: SHServ/Middleware/ControlScripts.php:24
Что: explode("\\", str_replace("\\Scopes", "", str_replace("SHServ", "", static::class))) — хрупко.
Фикс: basename(str_replace('\\', '/', static::class)).


9.31 🟡 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.


9.32 🟡 No correlation ID for event dispatch chain

Где: SHServ/Controllers/EventsController.php
Что: Нет tracing ID correlating original event with downstream device API calls.
Фикс: Generate correlation_id and propagate through handlers.


9.33 🟡 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.


9.34 🟡 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.


9.35 🟡 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