# Input Validation and Sanitization XC_VM uses a two-layer defense for incoming request data. First, a **global sanitization** pass strips dangerous content from all PHP superglobals during bootstrap, before any application code runs. Second, an **action-level validation** layer checks that required fields are present before business logic executes. Both layers are implemented in `src/Core/Validation/InputValidator.php`. --- ## Global Sanitization Flow Sanitization runs automatically during bootstrap. When `LegacyInitializer::initCore()` is called (in `src/Core/Init/LegacyInitializer.php`), it performs the following steps before any controller or service code: ``` LegacyInitializer::initCore() | +-- InputValidator::cleanGlobals($_GET) +-- InputValidator::cleanGlobals($_POST) +-- InputValidator::cleanGlobals($_SESSION) +-- InputValidator::cleanGlobals($_COOKIE) | +-- $input = InputValidator::parseIncomingRecursively($_GET) +-- RequestManager::set(InputValidator::parseIncomingRecursively($_POST, $input)) ``` After this sequence, all raw superglobals have been sanitized in-place, and the merged/cleaned GET+POST data is available through `RequestManager`. The streaming context (`LegacyInitializer::initStreaming()`) performs the same sanitization sequence using the `Request` class, which provides equivalent methods for the streaming bootstrap path. ### cleanGlobals(&$rData, $rIteration = 0) Recursively walks the given superglobal array and removes dangerous content. Applied to `$_GET`, `$_POST`, `$_SESSION`, and `$_COOKIE`. Strips the following from every scalar value: | Threat | Pattern removed | Replacement | | --- | --- | --- | | Null byte injection | `\0` (chr 0) | removed | | Path traversal | `../` | `../` (HTML-encoded) | | RTL override (UI spoofing) | `‮` | removed | Recursion is capped at 10 levels to prevent stack exhaustion from deeply nested input. ### parseIncomingRecursively(&$rData, $rInput, $rIteration = 0) Recursively walks GET and POST data, applying key and value sanitization to every leaf. For arrays, it recurses up to 20 levels deep. For scalar values, it applies `parseCleanKey()` to the key and `parseCleanValue()` to the value. The merged result (GET first, then POST overlaid) is stored in `RequestManager` for use throughout the request lifecycle. ### parseCleanKey($rKey) Sanitizes array keys to prevent injection through key names: 1. URL-decodes and HTML-escapes the key (`htmlspecialchars(urldecode(...))`) 2. Removes double-dot sequences (`..` -> `''`) 3. Strips `__dunder__`-style markers via regex 4. Validates against allowed character set: word characters, dots, hyphens, underscores ### parseCleanValue($rValue) Sanitizes scalar values with multiple passes: | Step | What it does | | --- | --- | | Unescape | `stripslashes()` and restore ` ` to space | | Normalize newlines | Convert `\r\n`, `\n\r`, `\r` to `\n` | | HTML comment protection | `` becomes `-->` | | Script tag neutralization | ` STATUS_INVALID_INPUT, 'data' => $data] return $error; } ``` ### confirmIDs($ids) ```php InputValidator::confirmIDs($ids) // untyped params/return; yields a filtered array of positive int IDs ``` Filters an array to contain only positive integer IDs. Any value where `intval($id) <= 0` is dropped. Used extensively across the codebase (30+ call sites) wherever user-supplied ID lists need to be sanitized before database queries. ```php $safeIds = InputValidator::confirmIDs($userSuppliedIds); // [1, 42, 7] -- negative, zero, and non-numeric values removed ``` --- ## Validation Action Reference The `validate()` method uses a `switch` statement over the action name. Actions are grouped below by functional area. ### Content Management #### Streams and Channels | Action | Required fields | Notes | | --- | --- | --- | | `processStream` | `stream_display_name` OR `review` flag OR `$_FILES['m3u_file']` | Any one of the three satisfies validation | | `processChannel` | `stream_display_name` OR `review` flag OR `$_FILES['m3u_file']` | Same rules as processStream | | `processRadio` | `stream_display_name` OR `review` flag OR `$_FILES['m3u_file']` | Same rules as processStream | #### Movies / VOD | Action | Required fields | Notes | | --- | --- | --- | | `processMovie` | `stream_display_name` OR `review` flag OR `$_FILES['m3u_file']` | Same rules as processStream | #### Series and Episodes | Action | Required fields | Notes | | --- | --- | --- | | `processSeries` | `title` | Series name is required | | `processEpisode` | `series` (non-empty) AND `season_num` (numeric) AND (`multi` flag OR `episode` numeric) | Complex multi-path validation | ### Organization #### Bouquets | Action | Required fields | Notes | | --- | --- | --- | | `processBouquet` | `bouquet_name` | Required scalar | | `reorderBouquet` | `stream_order_array` | Must decode to a JSON array | | `sortBouquets` | `bouquet_order_array` | Must decode to a JSON array | #### Categories | Action | Required fields | Notes | | --- | --- | --- | | `processCategory` | `category_name`, `category_type` | Both required | | `orderCategories` | `categories` | Must decode to a JSON array | #### Groups and Codes | Action | Required fields | Notes | | --- | --- | --- | | `processGroup` | `group_name` | Required scalar | | `processGroupLegacy` | `group_name` | Same as processGroup | | `processCode` | `code` | Required scalar | | `processPackage` | `package_name` | Required scalar | ### EPG | Action | Required fields | Notes | | --- | --- | --- | | `processEPG` | `epg_name`, `epg_file` | Both required | ### Devices and Lines | Action | Required fields | Notes | | --- | --- | --- | | `processMAG` | `mac` | MAC address required | | `processEnigma` | `mac` | MAC address required | | `setChannelOrder` | `stream_order_array` | Must decode to a JSON array | ### Profiles | Action | Required fields | Notes | | --- | --- | --- | | `processProfile` | `profile_name` | Required scalar | ### Providers | Action | Required fields | Notes | | --- | --- | --- | | `processProvider` | `ip`, `port`, `username`, `password`, `name` | All five required | | `processISP` | `isp` | Required scalar | | `processUA` | `user_agent` | Required scalar | ### Security | Action | Required fields | Notes | | --- | --- | --- | | `blockIP` | `ip` | IP address required | | `processRTMPIP` | `ip` | IP address required | ### Server Management | Action | Required fields | Notes | | --- | --- | --- | | `processServer` | `server_name`, `server_ip` | Both required | | `processProxy` | `server_name`, `server_ip` | Same as processServer | | `installServer` | `ssh_port`, `root_password` | Both required | | `moveStreams` | `content_type`, `source_server`, `replacement_server` | All three required | | `replaceDNS` | `old_dns`, `new_dns` | Both required | | `orderServers` | `server_order` | Must decode to a JSON array | ### Recordings and Watch Folders | Action | Required fields | Notes | | --- | --- | --- | | `scheduleRecording` | `title`, `source_id` | Both required | | `processWatchFolder` | `folder_type`, `selected_path`, `server_id` | All three required | ### Mass Operations (JSON Array Payloads) All mass operations require a JSON-encoded array in the specified field. The field must decode to a valid PHP array. | Action | JSON field | | --- | --- | | `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` | --- ## Default Fallthrough Behavior Actions not explicitly listed in the `switch` statement fall through to `return true`, meaning they always pass validation. This is intentional -- these actions either have no required fields at the gate level, or perform their own validation deeper in the business logic layer. **Explicitly pass-through actions** (listed in the switch with `return true`): - `processUser` - `processLine` - `processHMAC` - `editAdminProfile` - `editSettings` - `editBackupSettings` - `editCacheCron` - `editPlexSettings` - `editWatchSettings` - `processPlexSync` - `processLogin` - `submitTicket` **Implicitly pass-through actions** (not in the switch at all, caught by the default `return true`): Any action string not matching a `case` will also return `true`. If a new action needs input gating, a case must be added explicitly. --- ## Validation Patterns Used The `validate()` method uses a small set of patterns consistently: | Pattern | Purpose | Example | | --- | --- | --- | | `!empty($rData['field'])` | Required scalar field (non-null, non-empty, non-zero) | `!empty($rData['bouquet_name'])` | | `is_numeric($rData['field'] ?? null)` | Numeric validation with null-safe fallback | `is_numeric($rData['season_num'] ?? null)` | | `is_array(json_decode($rData['field'] ?? '', true))` | JSON string that must decode to an array | `is_array(json_decode($rData['streams'] ?? '', true))` | | `isset($rData['field'])` | Field presence check (value can be empty/falsy) | `isset($rData['review'])` | | `isset($_FILES['field'])` | File upload presence check | `isset($_FILES['m3u_file'])` | | OR conditions | Multi-path validation (any one path satisfies) | `!empty($rData['stream_display_name']) \|\| isset($rData['review']) \|\| isset($_FILES['m3u_file'])` | --- ## Adding Validation for a New Action Add a `case` to the `switch` in `src/Core/Validation/InputValidator.php`: ```php case 'myNewAction': return !empty($rData['required_field']) && is_numeric($rData['numeric_field'] ?? null); ``` Guidelines: - Validate only the minimum required inputs at this layer. Keep domain-specific rules (format validation, business constraints, uniqueness checks) in the service layer. - Use `!empty()` for required scalars, `is_numeric()` for numeric fields, and `is_array(json_decode(..., true))` for JSON array payloads. - For actions that accept file uploads as an alternative to form fields, include `isset($_FILES['field'])` as an OR condition. - If the action needs no gate-level validation, add it to the explicit pass-through block with `return true` so future maintainers know the omission is intentional rather than accidental. --- ## Related files | File | Purpose | | --- | --- | | `src/Core/Validation/InputValidator.php` | All sanitization and validation logic | | `src/Core/Init/LegacyInitializer.php` | Bootstrap caller that triggers sanitization via `initCore()` | | `src/Core/Http/RequestManager.php` | Stores the sanitized, merged GET+POST data | | `src/Public/Controllers/` | Controllers that call `validate()` / `validateOrFail()` before business logic |