{"id":"GHSA-8pv3-29pp-pf8f","summary":"WWBN AVideo has Stored XSS via Unanchored Duration Regex in Video Encoder Receiver","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-\u003esetDuration($_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\u003c/time\u003e\u003cimg src=x onerror=alert(1)\u003e\u003ctime\u003e` 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\u003c/time\u003e\u003cimg src=x onerror=alert(1)\u003e\u003ctime\u003e`, 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\u003ctime class=\"duration\"\u003e\u003c?php echo Video::getCleanDuration($value['duration']); ?\u003e\u003c/time\u003e\n```\n\n2. `view/include/playlist.php:159`:\n```php\n\u003ctime class=\"duration\"\u003e\u003c?php echo Video::getCleanDuration(@$value['duration']); ?\u003e\u003c/time\u003e\n```\n\n3. `objects/video.php:7200` (gallery thumbnail generation):\n```php\n$img .= \"\u003ctime class=\\\"duration\\\"...\u003e\" . $duration . \"\u003c/time\u003e\";\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\u003c/time\u003e\u003cimg src=x onerror=alert(document.cookie)\u003e\u003ctime\u003e'\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 `\u003ctime\u003e` 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- \u003ctime class=\"duration\"\u003e\u003c?php echo Video::getCleanDuration($value['duration']); ?\u003e\u003c/time\u003e\n+ \u003ctime class=\"duration\"\u003e\u003c?php echo htmlspecialchars(Video::getCleanDuration($value['duration']), ENT_QUOTES, 'UTF-8'); ?\u003e\u003c/time\u003e\n```\n\nIn `view/include/playlist.php:159`:\n```php\n- \u003ctime class=\"duration\"\u003e\u003c?php echo Video::getCleanDuration(@$value['duration']); ?\u003e\u003c/time\u003e\n+ \u003ctime class=\"duration\"\u003e\u003c?php echo htmlspecialchars(Video::getCleanDuration(@$value['duration']), ENT_QUOTES, 'UTF-8'); ?\u003e\u003c/time\u003e\n```\n\nIn `objects/video.php:7200`:\n```php\n- $img .= \"\u003ctime class=\\\"duration\\\"...\u003e\" . $duration . \"\u003c/time\u003e\";\n+ $img .= \"\u003ctime class=\\\"duration\\\"...\u003e\" . htmlspecialchars($duration, ENT_QUOTES, 'UTF-8') . \"\u003c/time\u003e\";\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.","aliases":["CVE-2026-41061"],"modified":"2026-05-05T16:11:40.860308Z","published":"2026-04-14T23:22:21Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-04-14T23:22:21Z","nvd_published_at":"2026-04-21T23:16:21Z","cwe_ids":["CWE-79"],"severity":"MODERATE"},"references":[{"type":"WEB","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-8pv3-29pp-pf8f"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-41061"},{"type":"WEB","url":"https://github.com/WWBN/AVideo/commit/bcba324644df8b4ed1f891462455f1cd26822a45"},{"type":"PACKAGE","url":"https://github.com/WWBN/AVideo"}],"affected":[{"package":{"name":"wwbn/avideo","ecosystem":"Packagist","purl":"pkg:composer/wwbn/avideo"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"29.0"}]}],"versions":["10.4","10.8","11","11.1","11.1.1","11.5","11.6","12.4","14.3","14.3.1","14.4","18.0","21.0","22.0","24.0","25.0","26.0","29.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-8pv3-29pp-pf8f/GHSA-8pv3-29pp-pf8f.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N"}]}