GHSA-8PV3-29PP-PF8F
Vulnerability from github – Published: 2026-04-14 23:22 – Updated: 2026-04-14 23:22Summary
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.
Details
Input entry point: objects/aVideoEncoderReceiveImage.json.php:208
// Line 203-211
if (!empty($_REQUEST['duration'])) {
$video->setDuration($_REQUEST['duration']);
}
Insufficient validation: objects/video.php:918
static function isValidDuration($duration) {
// ...
return preg_match('/^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}/', $duration);
// Missing $ anchor here -----------------------------------^
}
The 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.
No sanitization in output function: objects/video.php:3463-3480
public static function getCleanDuration($duration = "") {
$durationParts = explode(".", $duration);
$duration = $durationParts[0];
$durationParts = explode(':', $duration);
if (count($durationParts) == 1) {
return '0:00:' . static::addZero($durationParts[0]);
} elseif (count($durationParts) == 2) {
return '0:' . static::addZero($durationParts[0]) . ':' . static::addZero($durationParts[1]);
}
return $duration; // Returns full string unmodified for 3+ colon parts
}
With 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.
Unescaped output sinks:
view/trending.php:72:
<time class="duration"><?php echo Video::getCleanDuration($value['duration']); ?></time>
view/include/playlist.php:159:
<time class="duration"><?php echo Video::getCleanDuration(@$value['duration']); ?></time>
objects/video.php:7200(gallery thumbnail generation):
$img .= "<time class=\"duration\"...>" . $duration . "</time>";
No Content-Security-Policy headers are set. The application uses raw PHP templates with no auto-escaping framework.
PoC
-
Authenticate as a user with upload permission and obtain a
video_id_hashfor a video (visible in encoder API responses or via the upload flow). -
Send the malicious duration:
curl -X POST "https://target/objects/aVideoEncoderReceiveImage.json.php" \
-d "videos_id=VIDEO_ID" \
-d "video_id_hash=HASH" \
-d 'duration=00:00:01</time><img src=x onerror=alert(document.cookie)><time>'
-
The
isValidDuration()regex matches the00:00:01prefix and allows the full string to be stored. -
Visit the trending page (
/view/trending.php) or any playlist containing the poisoned video. The injected HTML breaks out of the<time>tag and theonerrorhandler executes JavaScript in the victim's browser context.
Impact
- 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).
- Account takeover: Stolen admin session cookies grant full platform control.
- Phishing: Attacker can inject fake login forms or redirect users to malicious sites.
- Worm potential: Since the XSS fires on commonly-visited listing pages (trending), it can propagate without targeted delivery — any visitor is a victim.
The 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).
Recommended Fix
Fix 1 — Anchor the regex (objects/video.php:918):
- return preg_match('/^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}/', $duration);
+ return preg_match('/^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}(\.[0-9]+)?$/', $duration);
Fix 2 — HTML-escape all duration output (defense in depth):
In view/trending.php:72:
- <time class="duration"><?php echo Video::getCleanDuration($value['duration']); ?></time>
+ <time class="duration"><?php echo htmlspecialchars(Video::getCleanDuration($value['duration']), ENT_QUOTES, 'UTF-8'); ?></time>
In view/include/playlist.php:159:
- <time class="duration"><?php echo Video::getCleanDuration(@$value['duration']); ?></time>
+ <time class="duration"><?php echo htmlspecialchars(Video::getCleanDuration(@$value['duration']), ENT_QUOTES, 'UTF-8'); ?></time>
In objects/video.php:7200:
- $img .= "<time class=\"duration\"...>" . $duration . "</time>";
+ $img .= "<time class=\"duration\"...>" . htmlspecialchars($duration, ENT_QUOTES, 'UTF-8') . "</time>";
Both 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.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "wwbn/avideo"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "29.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-79"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-14T23:22:21Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"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[\u0027duration\u0027])) {\n $video-\u003esetDuration($_REQUEST[\u0027duration\u0027]);\n}\n```\n\n**Insufficient validation:** `objects/video.php:918`\n\n```php\nstatic function isValidDuration($duration) {\n // ...\n return preg_match(\u0027/^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}/\u0027, $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(\u0027:\u0027, $duration);\n if (count($durationParts) == 1) {\n return \u00270:00:\u0027 . static::addZero($durationParts[0]);\n } elseif (count($durationParts) == 2) {\n return \u00270:\u0027 . static::addZero($durationParts[0]) . \u0027:\u0027 . 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[\u0027duration\u0027]); ?\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[\u0027duration\u0027]); ?\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 \u0027duration=00:00:01\u003c/time\u003e\u003cimg src=x onerror=alert(document.cookie)\u003e\u003ctime\u003e\u0027\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\u0027s 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 \u2014 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\u0027s duration (high blast radius).\n\n## Recommended Fix\n\n**Fix 1 \u2014 Anchor the regex** (`objects/video.php:918`):\n\n```php\n- return preg_match(\u0027/^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}/\u0027, $duration);\n+ return preg_match(\u0027/^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}(\\.[0-9]+)?$/\u0027, $duration);\n```\n\n**Fix 2 \u2014 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[\u0027duration\u0027]); ?\u003e\u003c/time\u003e\n+ \u003ctime class=\"duration\"\u003e\u003c?php echo htmlspecialchars(Video::getCleanDuration($value[\u0027duration\u0027]), ENT_QUOTES, \u0027UTF-8\u0027); ?\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[\u0027duration\u0027]); ?\u003e\u003c/time\u003e\n+ \u003ctime class=\"duration\"\u003e\u003c?php echo htmlspecialchars(Video::getCleanDuration(@$value[\u0027duration\u0027]), ENT_QUOTES, \u0027UTF-8\u0027); ?\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, \u0027UTF-8\u0027) . \"\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.",
"id": "GHSA-8pv3-29pp-pf8f",
"modified": "2026-04-14T23:22:21Z",
"published": "2026-04-14T23:22:21Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-8pv3-29pp-pf8f"
},
{
"type": "WEB",
"url": "https://github.com/WWBN/AVideo/commit/bcba324644df8b4ed1f891462455f1cd26822a45"
},
{
"type": "PACKAGE",
"url": "https://github.com/WWBN/AVideo"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "WWBN AVideo has Stored XSS via Unanchored Duration Regex in Video Encoder Receiver"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.