{"id":"GHSA-wfq5-qgqp-hvhv","summary":"Unauthenticated Reflected XSS via innerHTML in AVideo","details":"## Summary\n\nAVideo contains a reflected XSS vulnerability that allows unauthenticated attackers to execute arbitrary JavaScript in a victim's browser. User input from a URL parameter flows through PHP's `json_encode()` into a JavaScript function that renders it via `innerHTML`, bypassing encoding and achieving full script execution.\n\n\n## Root Cause\nThe vulnerability is caused by two issues working together:\n\n### 1. Source: Unescaped user input passed to JavaScript (videoNotFound.php)\n\n**File:** `view/videoNotFound.php` line 49\n\n```php\nif (!empty($_REQUEST['404ErrorMsg'])) {\n    echo 'avideoAlertInfo(' . json_encode($_REQUEST['404ErrorMsg']) . ');';\n}\n```\n\nPHP's `json_encode()` with default flags only escapes quotes (`\"` → `\\\"`) and backslashes. It does **NOT** escape HTML special characters (`\u003c`, `\u003e`, `/`). The resulting string contains raw HTML tags that are passed directly to JavaScript.\n\n### 2. Sink: innerHTML renders HTML tags as executable DOM (script.js)\n\n**File:** `view/js/script.js`\n\n```javascript\nfunction avideoAlertInfo(msg) {            // line ~1891\n    avideoAlert(\"\", msg, 'info');           // calls ↓\n}\n\nfunction avideoAlert(title, msg, type) {   // line ~1270\n    avideoAlertHTMLText(title, msg, type);  // calls ↓\n}\n\nfunction avideoAlertHTMLText(title, msg, type) {  // line ~1451\n    var span = document.createElement(\"span\");\n    span.innerHTML = msg;                  // line 1464 — XSS SINK\n    swal({ content: span });\n}\n```\n\n`innerHTML` parses the string as HTML. Any `\u003cimg\u003e`, `\u003csvg\u003e`, or other HTML tags with event handlers are instantiated as real DOM elements, triggering JavaScript execution.\n\n### Data Flow\n\n```\nURL parameter (?404ErrorMsg=PAYLOAD)\n    → $_REQUEST['404ErrorMsg']\n    → json_encode()          ← does NOT escape \u003c \u003e /\n    → avideoAlertInfo()\n    → avideoAlert()\n    → avideoAlertHTMLText()\n    → span.innerHTML = msg   ← renders HTML tags, executes JS\n```\n\n---\n\n## Proof of Concept\n```\nhttps://localhost/view/videoNotFound.php?404ErrorMsg=\u003cimg src=x onerror=alert(document.domain)\u003e\n```\n\u003cimg width=\"1918\" height=\"1035\" alt=\"image\" src=\"https://github.com/user-attachments/assets/20077ce2-5b49-4bd3-a7df-ab48be786cc1\" /\u003e\n\nThe page renders:\n```javascript\navideoAlertInfo(\"\u003cimg src=x onerror=alert(document.domain)\u003e\");\n```\n\nWhich flows to `span.innerHTML = \"\u003cimg src=x onerror=alert(document.domain)\u003e\"`. The browser creates an `\u003cimg\u003e` element, `src=x` fails to load, `onerror` fires `alert(document.domain)`.\n\n## Affected Code\n\n| File | Line | Issue |\n|------|------|-------|\n| `view/videoNotFound.php` | 49 | `json_encode()` does not escape `\u003c` `\u003e` for HTML context |\n| `view/js/script.js` | 1464 | `span.innerHTML = msg` renders user input as HTML |\n| `view/js/script.js` | 1282 | `span.innerHTML = msg` in `avideoAlertWithCookie()` |\n| `view/js/script.js` | 1335 | `span.innerHTML = __(msg,true)` in `avideoConfirm()` |\n| `view/js/script.js` | 1358 | `span.innerHTML = msg` in `avideoAlertOnceForceConfirm()` |\n\nThe `innerHTML` sink exists in 4 functions. Any future code that passes user input to `avideoAlertInfo()`, `avideoAlertWarning()`, `avideoAlertDanger()`, or `avideoAlertSuccess()` will create additional XSS vectors.\n\n\n## Remediation\n\n### Fix 1: Escape HTML in PHP (source fix)\n\n```php\n// view/videoNotFound.php line 49\n// BEFORE (vulnerable):\necho 'avideoAlertInfo(' . json_encode($_REQUEST['404ErrorMsg']) . ');';\n\n// AFTER (fixed):\necho 'avideoAlertInfo(' . json_encode($_REQUEST['404ErrorMsg'], JSON_HEX_TAG | JSON_HEX_AMP) . ');';\n```\n\n`JSON_HEX_TAG` converts `\u003c` → `\\u003C` and `\u003e` → `\\u003E`, preventing HTML injection.\n\n### Fix 2: Use textContent instead of innerHTML (sink fix, recommended)\n\n```javascript\n// view/js/script.js - all alert functions\n// BEFORE (vulnerable):\nspan.innerHTML = msg;\n\n// AFTER (fixed):\nspan.textContent = msg;\n```\n\n`textContent` treats the string as plain text — HTML tags are displayed literally, never parsed or executed.\n\n### Fix 3: Add Content-Security-Policy header (defense in depth)\n\n```\nContent-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'\n```\n\n## Impact\n\n- **Session hijacking** — steal `PHPSESSID` cookie (not HttpOnly by default)\n- **Account takeover** — use stolen session to change password or email\n- **Phishing** — inject a realistic login form inside the SweetAlert modal\n- **Worm propagation** — inject self-spreading payloads via comments/messages\n- **Admin compromise** — send crafted link to admin, steal session, gain full control","aliases":["CVE-2026-33035"],"modified":"2026-03-20T21:37:42.693809Z","published":"2026-03-17T20:05:23Z","database_specific":{"nvd_published_at":"2026-03-20T05:16:16Z","cwe_ids":["CWE-79"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-03-17T20:05:23Z"},"references":[{"type":"WEB","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-wfq5-qgqp-hvhv"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33035"},{"type":"WEB","url":"https://github.com/WWBN/AVideo/commit/cca6196f4072cb9acc39b1030fb8fb1702b4f69b"},{"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":"25.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"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-wfq5-qgqp-hvhv/GHSA-wfq5-qgqp-hvhv.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N"}]}