{"id":"CVE-2026-41061","aliases":["EUVD-2026-24539","GHSA-8pv3-29pp-pf8f"],"status":"published","title":"WWBN AVideo Vulnerable to stored XSS via Unanchored Duration Regex in Video Encoder Receiver","description":"WWBN AVideo is an open source video platform. In versions 29.0 and below, the `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. Commit bcba324644df8b4ed1f891462455f1cd26822a45 contains a fix.","severity":{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-12","score":0.00173,"percentile":0.06802,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":false},"cwes":["CWE-79"],"published":"2026-04-14T23:22:21Z","modified":"2026-10-28T02:30:43Z","affected":[{"source":"ghsa","ecosystem":"Packagist","package":"wwbn/avideo","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"29.0"}]}]},{"source":"osv","ecosystem":"Packagist","package":"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"]}],"products":[{"vendor":"wwbn","product":"avideo"}],"references":[{"url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-8pv3-29pp-pf8f","tags":["x_refsource_CONFIRM","exploit","Exploit","Vendor Advisory","advisory","web"]},{"url":"https://github.com/WWBN/AVideo/commit/bcba324644df8b4ed1f891462455f1cd26822a45","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-41061","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-8pv3-29pp-pf8f"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/41xxx/CVE-2026-41061.json","tags":["advisory"]},{"url":"https://github.com/WWBN/AVideo","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-41061","title":"WWBN AVideo Vulnerable to stored XSS via Unanchored Duration Regex in Video Encoder Receiver","description":"WWBN AVideo is an open source video platform. In versions 29.0 and below, the `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. Commit bcba324644df8b4ed1f891462455f1cd26822a45 contains a fix.","status":"published","published":"2026-04-21T22:49:40.623Z","modified":"2026-04-22T13:14:27.8Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T13:41:08.498858Z"}],"euvd":[{"id":"EUVD-2026-24539","description":"WWBN AVideo is an open source video platform. In versions 29.0 and below, the `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. Commit bcba324644df8b4ed1f891462455f1cd26822a45 contains a fix.","status":"published","published":"2026-04-21T22:49:40Z","modified":"2026-04-22T13:14:27Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T19:24:21.101387Z"}],"ghsa":[{"id":"GHSA-8pv3-29pp-pf8f","title":"WWBN AVideo has Stored XSS via Unanchored Duration Regex in Video Encoder Receiver","description":"## 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.","status":"published","published":"2026-04-14T23:22:21Z","modified":"2026-04-24T20:41:09Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:39:56.453138Z"}],"nvd":[{"id":"CVE-2026-41061","description":"WWBN AVideo is an open source video platform. In versions 29.0 and below, the `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. Commit bcba324644df8b4ed1f891462455f1cd26822a45 contains a fix.","status":"published","published":"2026-04-21T23:16:21.387Z","modified":"2026-10-26T21:10:00.17Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:12:15.493943Z"}],"osv":[{"id":"CVE-2026-41061","title":"WWBN AVideo Vulnerable to stored XSS via Unanchored Duration Regex in Video Encoder Receiver","description":"WWBN AVideo is an open source video platform. In versions 29.0 and below, the `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. Commit bcba324644df8b4ed1f891462455f1cd26822a45 contains a fix.","status":"published","published":"2026-04-21T22:49:40.623Z","modified":"2026-10-28T02:30:43.305693134Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T15:45:03.424067Z"},{"id":"GHSA-8pv3-29pp-pf8f","title":"WWBN AVideo has Stored XSS via Unanchored Duration Regex in Video Encoder Receiver","description":"## 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.","status":"published","published":"2026-04-14T23:22:21Z","modified":"2026-05-05T16:11:40.860308Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T15:46:42.78452Z"}]}}
