Files
XC_VM/docs/ru/guides/input-validation.md
T
Divarion-D 76844fef11 docs: restructure, fix PSR-4 drift, and unify en/ru
Overhaul the Docsify documentation (English + Russian) so it matches the current
codebase and follows one consistent pattern.

Content accuracy (post-migration):
- Rewrite development/autoloader.md to PSR-4 / Composer (the old XC_Autoloader
  scanner, igbinary tmp/cache/autoload_map and registerDirectories are gone).
- PascalCase every source path (src/core -> src/Core, domain/Stream, cli/Commands,
  public/Controllers, Infrastructure/Redis, ...) across all docs.
- Replace the removed autoload.php references with vendor/autoload.php
  (build_system, bootstrap-contexts, error-handling, modules).
- ssl-generation: note that the installer now auto-generates a unique self-signed
  certificate before Nginx starts.

Common pattern (Clean & uniform):
- Strip emoji from headings; remove the in-page Navigation blocks (the Docsify
  sidebar already provides navigation).
- One H1 + intro per doc; uniform "Related files" / "Связанные файлы" section,
  added to the code-centric docs that lacked it.

Structure:
- Remove the empty stray docs/api/; move updates_checklist.md into builds/;
  link the previously-orphaned ucs-integration.md.
- Regroup the sidebars (split the oversized guides group into Developer Guides /
  Security & Access / Integrations; fold builds into Build & Release).

Augment:
- dev-workflow: Local Setup (make dev-tools) + Quality Checks (phpstan, cs, gates).
- build_system: Composer Dependencies section (committed prod-only vendor,
  committed lock, dev tools via composer install, no build-time vendor step).

en/ru parity:
- Apply the same structure, fixes and pattern to docs/ru/ (translated), including
  a new Russian ucs-integration.md. The en and ru file sets are now identical.
2026-06-26 15:56:15 +03:00

321 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Валидация и санитизация ввода
XC_VM использует двухуровневую защиту входящих данных запроса. Сначала **глобальная санитизация** удаляет опасный контент из всех PHP-суперглобалов во время bootstrap, до того как заработает прикладной код. Затем **валидация на уровне действий** проверяет наличие обязательных полей перед выполнением бизнес-логики.
Оба слоя реализованы в `src/Core/Validation/InputValidator.php`.
---
## Поток глобальной санитизации
Санитизация запускается автоматически при bootstrap. Когда вызывается `LegacyInitializer::initCore()` (в `src/Core/Init/LegacyInitializer.php`), он выполняет следующие шаги до любого контроллера или сервиса:
```
LegacyInitializer::initCore()
|
+-- InputValidator::cleanGlobals($_GET)
+-- InputValidator::cleanGlobals($_POST)
+-- InputValidator::cleanGlobals($_SESSION)
+-- InputValidator::cleanGlobals($_COOKIE)
|
+-- $input = InputValidator::parseIncomingRecursively($_GET)
+-- RequestManager::set(InputValidator::parseIncomingRecursively($_POST, $input))
```
После этой последовательности все сырые суперглобалы санитизированы in-place, а объединённые/очищенные данные GET+POST доступны через `RequestManager`.
Стриминговый контекст (`LegacyInitializer::initStreaming()`) выполняет ту же последовательность санитизации, используя класс `Request`, который предоставляет эквивалентные методы для bootstrap-пути стриминга.
### cleanGlobals(&$rData, $rIteration = 0)
Рекурсивно обходит заданный массив суперглобала и удаляет опасный контент. Применяется к `$_GET`, `$_POST`, `$_SESSION` и `$_COOKIE`.
Удаляет следующее из каждого скалярного значения:
| Угроза | Удаляемый паттерн | Замена |
| --- | --- | --- |
| Null byte injection | `\0` (chr 0) | удаляется |
| Path traversal | `../` | `../` (HTML-encoded) |
| RTL override (UI spoofing) | `‮` | удаляется |
Рекурсия ограничена 10 уровнями для предотвращения исчерпания стека при глубоко вложенном вводе.
### parseIncomingRecursively(&$rData, $rInput, $rIteration = 0)
Рекурсивно обходит данные GET и POST, применяя санитизацию ключей и значений к каждому листу. Для массивов рекурсирует до 20 уровней. Для скалярных значений применяет `parseCleanKey()` к ключу и `parseCleanValue()` к значению.
Объединённый результат (сначала GET, затем POST поверх него) сохраняется в `RequestManager` для использования в течение жизненного цикла запроса.
### parseCleanKey($rKey)
Санитизирует ключи массивов для предотвращения инъекций через имена ключей:
1. URL-декодирует и HTML-экранирует ключ (`htmlspecialchars(urldecode(...))`)
2. Удаляет последовательности двойных точек (`..` -> `''`)
3. Удаляет маркеры в стиле `__dunder__` через regex
4. Валидирует по разрешённому набору символов: словесные символы, точки, дефисы, подчёркивания
### parseCleanValue($rValue)
Санитизирует скалярные значения в несколько проходов:
| Шаг | Что делает |
| --- | --- |
| Unescape | `stripslashes()` и восстановление ` ` в пробел |
| Нормализация переводов строк | Конвертирует `\r\n`, `\n\r`, `\r` в `\n` |
| Защита HTML-комментариев | `<!--` становится `&#60;&#33;--`, `-->` становится `--&#62;` |
| Нейтрализация script-тегов | `<script` (регистронезависимо) становится `&#60;script` |
| Нормализация сущностей | Исправление двойного-кодирования и некорректных числовых сущностей |
| Trim | Удаление ведущих/завершающих пробелов |
---
## Валидация на уровне действий
### validate()
```php
InputValidator::validate(string $rAction, array $rData): bool
```
Проверяет наличие минимально требуемых полей для заданного действия. Возвращает `true`, если данные приемлемы, и `false`, если обязательные поля отсутствуют или некорректны. Контроллеры должны вызывать её перед передачей данных в слой сервисов/репозиториев.
```php
if (!InputValidator::validate($action, $data)) {
// отклонить с ошибкой валидации
}
```
### validateOrFail()
```php
InputValidator::validateOrFail(string $rAction, array $rData): ?array
```
Удобная обёртка над `validate()`. Возвращает `null`, если данные валидны, или массив с ошибкой, если валидация не прошла:
```php
$error = InputValidator::validateOrFail($action, $data);
if ($error !== null) {
// $error = ['status' => STATUS_INVALID_INPUT, 'data' => $data]
return $error;
}
```
### confirmIDs($ids)
```php
InputValidator::confirmIDs(array $ids): array
```
Фильтрует массив, оставляя только положительные целочисленные ID. Любое значение, где `intval($id) <= 0`, отбрасывается. Используется широко по кодовой базе (30+ мест вызова) везде, где пользовательские списки ID должны быть санитизированы перед запросами к базе.
```php
$safeIds = InputValidator::confirmIDs($userSuppliedIds);
// [1, 42, 7] — отрицательные, ноль и нечисловые значения удалены
```
---
## Справочник действий валидации
Метод `validate()` использует `switch` по имени действия. Действия сгруппированы по функциональным областям ниже.
### Управление контентом
#### Потоки и каналы
| Действие | Обязательные поля | Заметки |
| --- | --- | --- |
| `processStream` | `stream_display_name` ИЛИ флаг `review` ИЛИ `$_FILES['m3u_file']` | Любое из трёх удовлетворяет валидации |
| `processChannel` | `stream_display_name` ИЛИ флаг `review` ИЛИ `$_FILES['m3u_file']` | Те же правила, что у processStream |
| `processRadio` | `stream_display_name` ИЛИ флаг `review` ИЛИ `$_FILES['m3u_file']` | Те же правила, что у processStream |
#### Movies / VOD
| Действие | Обязательные поля | Заметки |
| --- | --- | --- |
| `processMovie` | `stream_display_name` ИЛИ флаг `review` ИЛИ `$_FILES['m3u_file']` | Те же правила, что у processStream |
#### Сериалы и эпизоды
| Действие | Обязательные поля | Заметки |
| --- | --- | --- |
| `processSeries` | `title` | Имя сериала обязательно |
| `processEpisode` | `series` (непустое) И `season_num` (numeric) И (флаг `multi` ИЛИ `episode` numeric) | Сложная мульти-путевая валидация |
### Организация
#### Букеты
| Действие | Обязательные поля | Заметки |
| --- | --- | --- |
| `processBouquet` | `bouquet_name` | Обязательный скаляр |
| `reorderBouquet` | `stream_order_array` | Должен декодироваться в JSON-массив |
| `sortBouquets` | `bouquet_order_array` | Должен декодироваться в JSON-массив |
#### Категории
| Действие | Обязательные поля | Заметки |
| --- | --- | --- |
| `processCategory` | `category_name`, `category_type` | Оба обязательны |
| `orderCategories` | `categories` | Должен декодироваться в JSON-массив |
#### Группы и коды
| Действие | Обязательные поля | Заметки |
| --- | --- | --- |
| `processGroup` | `group_name` | Обязательный скаляр |
| `processGroupLegacy` | `group_name` | То же, что processGroup |
| `processCode` | `code` | Обязательный скаляр |
| `processPackage` | `package_name` | Обязательный скаляр |
### EPG
| Действие | Обязательные поля | Заметки |
| --- | --- | --- |
| `processEPG` | `epg_name`, `epg_file` | Оба обязательны |
### Устройства и линии
| Действие | Обязательные поля | Заметки |
| --- | --- | --- |
| `processMAG` | `mac` | MAC-адрес обязателен |
| `processEnigma` | `mac` | MAC-адрес обязателен |
| `setChannelOrder` | `stream_order_array` | Должен декодироваться в JSON-массив |
### Профили
| Действие | Обязательные поля | Заметки |
| --- | --- | --- |
| `processProfile` | `profile_name` | Обязательный скаляр |
### Провайдеры
| Действие | Обязательные поля | Заметки |
| --- | --- | --- |
| `processProvider` | `ip`, `port`, `username`, `password`, `name` | Все пять обязательны |
| `processISP` | `isp` | Обязательный скаляр |
| `processUA` | `user_agent` | Обязательный скаляр |
### Безопасность
| Действие | Обязательные поля | Заметки |
| --- | --- | --- |
| `blockIP` | `ip` | IP-адрес обязателен |
| `processRTMPIP` | `ip` | IP-адрес обязателен |
### Управление серверами
| Действие | Обязательные поля | Заметки |
| --- | --- | --- |
| `processServer` | `server_name`, `server_ip` | Оба обязательны |
| `processProxy` | `server_name`, `server_ip` | То же, что processServer |
| `installServer` | `ssh_port`, `root_password` | Оба обязательны |
| `moveStreams` | `content_type`, `source_server`, `replacement_server` | Все три обязательны |
| `replaceDNS` | `old_dns`, `new_dns` | Оба обязательны |
| `orderServers` | `server_order` | Должен декодироваться в JSON-массив |
### Записи и Watch Folders
| Действие | Обязательные поля | Заметки |
| --- | --- | --- |
| `scheduleRecording` | `title`, `source_id` | Оба обязательны |
| `processWatchFolder` | `folder_type`, `selected_path`, `server_id` | Все три обязательны |
### Массовые операции (JSON-массивы)
Все массовые операции требуют JSON-encoded массив в указанном поле. Поле должно декодироваться в валидный PHP-массив.
| Действие | JSON-поле |
| --- | --- |
| `massEditEpisodes` | `streams` |
| `massEditMovies` | `streams` |
| `massEditRadios` | `streams` |
| `massEditStreams` | `streams` |
| `massEditChannels` | `streams` |
| `massDeleteStreams` | `streams` |
| `massEditSeries` | `series` |
| `massDeleteSeries` | `series` |
| `massEditLines` | `users_selected` |
| `massEditUsers` | `users_selected` |
| `massEditMags` | `devices_selected` |
| `massEditEnigmas` | `devices_selected` |
| `massDeleteMovies` | `movies` |
| `massDeleteLines` | `lines` |
| `massDeleteUsers` | `users` |
| `massDeleteStations` | `radios` |
| `massDeleteMags` | `mags` |
| `massDeleteEnigmas` | `enigmas` |
| `massDeleteEpisodes` | `episodes` |
---
## Поведение по умолчанию (fallthrough)
Действия, явно не перечисленные в `switch`, проваливаются на `return true`, то есть всегда проходят валидацию. Это намеренно — у этих действий либо нет обязательных полей на уровне гейта, либо они выполняют собственную валидацию глубже в слое бизнес-логики.
**Действия с явным пропуском** (в switch с `return true`):
- `processUser`
- `processLine`
- `processHMAC`
- `editAdminProfile`
- `editSettings`
- `editBackupSettings`
- `editCacheCron`
- `editPlexSettings`
- `editWatchSettings`
- `processPlexSync`
- `processLogin`
- `submitTicket`
**Действия с неявным пропуском** (вообще не в switch, попадают в default `return true`):
Любая строка действия, не совпадающая с `case`, также вернёт `true`. Если новому действию нужен input-гейт, case должен быть добавлен явно.
---
## Используемые паттерны валидации
Метод `validate()` использует небольшой набор паттернов последовательно:
| Паттерн | Назначение | Пример |
| --- | --- | --- |
| `!empty($rData['field'])` | Обязательное скалярное поле (не null, не пустое, не ноль) | `!empty($rData['bouquet_name'])` |
| `is_numeric($rData['field'] ?? null)` | Числовая валидация с null-safe fallback | `is_numeric($rData['season_num'] ?? null)` |
| `is_array(json_decode($rData['field'] ?? '', true))` | JSON-строка, декодируемая в массив | `is_array(json_decode($rData['streams'] ?? '', true))` |
| `isset($rData['field'])` | Проверка наличия поля (значение может быть пустым/falsy) | `isset($rData['review'])` |
| `isset($_FILES['field'])` | Проверка наличия загруженного файла | `isset($_FILES['m3u_file'])` |
| OR-условия | Мульти-путевая валидация (любой путь удовлетворяет) | `!empty($rData['stream_display_name']) \|\| isset($rData['review']) \|\| isset($_FILES['m3u_file'])` |
---
## Добавление валидации для нового действия
Добавьте `case` в `switch` в `src/Core/Validation/InputValidator.php`:
```php
case 'myNewAction':
return !empty($rData['required_field'])
&& is_numeric($rData['numeric_field'] ?? null);
```
Рекомендации:
- Валидируйте только минимально необходимые входные данные на этом слое. Доменные правила (валидация формата, бизнес-ограничения, проверки уникальности) держите в сервисном слое.
- Используйте `!empty()` для обязательных скаляров, `is_numeric()` для числовых полей и `is_array(json_decode(..., true))` для JSON-массивов.
- Для действий, принимающих загрузку файлов как альтернативу полям формы, включите `isset($_FILES['field'])` как OR-условие.
- Если действию не нужна валидация на уровне гейта, добавьте его в блок явного пропуска с `return true`, чтобы будущим мейнтейнерам было понятно, что пропуск намеренный, а не случайный.
---
## Связанные файлы
| Файл | Назначение |
| --- | --- |
| `src/Core/Validation/InputValidator.php` | Вся логика санитизации и валидации |
| `src/Core/Init/LegacyInitializer.php` | Bootstrap-вызыватель, запускающий санитизацию через `initCore()` |
| `src/Core/Http/RequestManager.php` | Хранит санитизированные, объединённые данные GET+POST |
| `src/Public/Controllers/` | Контроллеры, вызывающие `validate()` / `validateOrFail()` перед бизнес-логикой |