# Аудит сервера 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())` — низкая энтропия, предсказуем.

**Фикс:**
```php
$token = bin2hex(random_bytes(32));
```

---

### 1.6 🟠 Cookie с защитными флагами

**Где:** `server/SHServ/Sessions.php:38`

**Что:** `setcookie("auth_token", ...)` без `HttpOnly`, `Secure`, `SameSite`. Увеличивает impact XSS.

**Фикс:**
```php
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`

**Что:**
```php
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'ами.

**Фикс:**
```php
$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. Клиенты (и прокси) не могут отличить ошибку от успеха по статусу.

**Фикс:**
```php
function response_error($msg, $alias = "error", $status_code = 400) {
    http_response_code($status_code);
    // ...
}
```
Контроллеры передают подходящий код: 400 для клиентских, 500 для серверных.

---

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

**Где:**
- `server/SHServ/Models/Areas.php:103-105` — `remove_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-77` — `new_area` не валидирует `type`, `alias`, `display_name`
- `server/SHServ/Controllers/DevicesRESTAPIController.php:32` — IP проверяется только `strlen < 7`
- `server/SHServ/Controllers/ScriptsRESTAPIController.php:19-29` — `run_action_script` передаёт сырые `$params` в callable без валидации
- `server/SHServ/Controllers/DevicesRESTAPIController.php:206` — `do_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-обёртка:
```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.

**Фикс:**
```php
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:177` — `device_status`
- `server/SHServ/RequiredControlScriptsScope.php:12-21` — `online` 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`

**Что:**
```php
array_filter($scripts_dir, function($item) {
    return !is_dir($item) and ...;
});
```
`$item` — имя без пути, `is_dir` проверяет текущую директорию, а не `ControlScripts/Scopes/`.

**Фикс:**
```php
!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`

**Что:**
```php
list($root) = explode('SHServ', __DIR__);
```
Сломается, если директория переименуется.

**Фикс:**
```php
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()`.

### Запуск
```bash
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.

---

## 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`.

**Фикс:** `DeviceAuth::kill()` вызывает `$device->clear_device_auth_cache()` через back-reference, переданную при создании инстанса.
**Статус:** ✅ **Исправлено.**

---

### 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 🟡 `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).

---

### 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()` и бросать исключение, если строка отсутствует.
**Статус:** ✅ **Исправлено.**

---

### 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_]*$/`.

**Фикс:** `/^[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. При конкурентных запросах возможен дубль.

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

---

### 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" != 123` → `false`, пропускает. Нет 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 |
