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

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

Дата: 2026-06-02
Версия 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'а.

Блокер: пока 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.

Блокер: валидация входных данных бессмысленна, если за ней всё равно стоит уязвимый 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 — Коммуникация с устройствами

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

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 — Качество кода и техдолг

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

Можно выполнять параллельно с 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.

Приложение: Файлы, требующие внимания

Файл Phase Проблемы
SHServ/config.php 1.3 Secrets, debug mode
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
SHServ/Sessions.php 1.5, 1.6 Токены, cookie flags
SHServ/Models/Example_Auth.php 1.4 SHA1
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
SHServ/Controllers/EventsController.php 3.1 Валидация event
SHServ/Models/Devices.php 2.3 Нет транзакции
SHServ/Models/Areas.php 5.10 Magic number, пустой stub
SHServ/Models/Scripts.php 1.7 unlink без auth
SHServ/Tools/DeviceScanner.php 4.1 253 concurrent curl
SHServ/Tools/DeviceAPI/Base.php 4.2, 4.3 Нет retry, hardcoded timeouts
SHServ/Tools/DeviceAPI/Hatch.php 5.6 Undefined variable
Fury/Modules/ThinBuilder/ThinBuilder.php 1.2, 2.4 SQL injection, silent null
Fury/Modules/ThinBuilder/ThinBuilderProcessing.php 1.2 SQL injection
Fury/Modules/ErrorHandler/ErrorHandler.php 2.1, 2.2 Инвертированная логика, закомментированный handler
Fury/Kernel/Logging.php 5.11 Логи в web root
console.php 1.7 Слив конфига