GHSA-WFQ5-QGQP-HVHV

Vulnerability from github – Published: 2026-03-17 20:05 – Updated: 2026-03-20 21:22
VLAI
Summary
Unauthenticated Reflected XSS via innerHTML in AVideo
Details

Summary

AVideo 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.

Root Cause

The vulnerability is caused by two issues working together:

1. Source: Unescaped user input passed to JavaScript (videoNotFound.php)

File: view/videoNotFound.php line 49

if (!empty($_REQUEST['404ErrorMsg'])) {
    echo 'avideoAlertInfo(' . json_encode($_REQUEST['404ErrorMsg']) . ');';
}

PHP's json_encode() with default flags only escapes quotes (" → \") and backslashes. It does NOT escape HTML special characters (<, >, /). The resulting string contains raw HTML tags that are passed directly to JavaScript.

2. Sink: innerHTML renders HTML tags as executable DOM (script.js)

File: view/js/script.js

function avideoAlertInfo(msg) {            // line ~1891
    avideoAlert("", msg, 'info');           // calls ↓
}

function avideoAlert(title, msg, type) {   // line ~1270
    avideoAlertHTMLText(title, msg, type);  // calls ↓
}

function avideoAlertHTMLText(title, msg, type) {  // line ~1451
    var span = document.createElement("span");
    span.innerHTML = msg;                  // line 1464 — XSS SINK
    swal({ content: span });
}

innerHTML parses the string as HTML. Any <img>, <svg>, or other HTML tags with event handlers are instantiated as real DOM elements, triggering JavaScript execution.

Data Flow

URL parameter (?404ErrorMsg=PAYLOAD)
    → $_REQUEST['404ErrorMsg']
    → json_encode()          ← does NOT escape < > /
    → avideoAlertInfo()
    → avideoAlert()
    → avideoAlertHTMLText()
    → span.innerHTML = msg   ← renders HTML tags, executes JS

Proof of Concept

https://localhost/view/videoNotFound.php?404ErrorMsg=<img src=x onerror=alert(document.domain)>

image

The page renders:

avideoAlertInfo("<img src=x onerror=alert(document.domain)>");

Which flows to span.innerHTML = "<img src=x onerror=alert(document.domain)>". The browser creates an <img> element, src=x fails to load, onerror fires alert(document.domain).

Affected Code

File Line Issue
view/videoNotFound.php 49 json_encode() does not escape < > for HTML context
view/js/script.js 1464 span.innerHTML = msg renders user input as HTML
view/js/script.js 1282 span.innerHTML = msg in avideoAlertWithCookie()
view/js/script.js 1335 span.innerHTML = __(msg,true) in avideoConfirm()
view/js/script.js 1358 span.innerHTML = msg in avideoAlertOnceForceConfirm()

The innerHTML sink exists in 4 functions. Any future code that passes user input to avideoAlertInfo(), avideoAlertWarning(), avideoAlertDanger(), or avideoAlertSuccess() will create additional XSS vectors.

Remediation

Fix 1: Escape HTML in PHP (source fix)

// view/videoNotFound.php line 49
// BEFORE (vulnerable):
echo 'avideoAlertInfo(' . json_encode($_REQUEST['404ErrorMsg']) . ');';

// AFTER (fixed):
echo 'avideoAlertInfo(' . json_encode($_REQUEST['404ErrorMsg'], JSON_HEX_TAG | JSON_HEX_AMP) . ');';

JSON_HEX_TAG converts < → \u003C and > → \u003E, preventing HTML injection.

Fix 2: Use textContent instead of innerHTML (sink fix, recommended)

// view/js/script.js - all alert functions
// BEFORE (vulnerable):
span.innerHTML = msg;

// AFTER (fixed):
span.textContent = msg;

textContent treats the string as plain text — HTML tags are displayed literally, never parsed or executed.

Fix 3: Add Content-Security-Policy header (defense in depth)

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'

Impact

  • Session hijacking — steal PHPSESSID cookie (not HttpOnly by default)
  • Account takeover — use stolen session to change password or email
  • Phishing — inject a realistic login form inside the SweetAlert modal
  • Worm propagation — inject self-spreading payloads via comments/messages
  • Admin compromise — send crafted link to admin, steal session, gain full control
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "wwbn/avideo"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "25.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-33035"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-79"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-17T20:05:23Z",
    "nvd_published_at": "2026-03-20T05:16:16Z",
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nAVideo contains a reflected XSS vulnerability that allows unauthenticated attackers to execute arbitrary JavaScript in a victim\u0027s browser. User input from a URL parameter flows through PHP\u0027s `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[\u0027404ErrorMsg\u0027])) {\n    echo \u0027avideoAlertInfo(\u0027 . json_encode($_REQUEST[\u0027404ErrorMsg\u0027]) . \u0027);\u0027;\n}\n```\n\nPHP\u0027s `json_encode()` with default flags only escapes quotes (`\"` \u2192 `\\\"`) 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, \u0027info\u0027);           // calls \u2193\n}\n\nfunction avideoAlert(title, msg, type) {   // line ~1270\n    avideoAlertHTMLText(title, msg, type);  // calls \u2193\n}\n\nfunction avideoAlertHTMLText(title, msg, type) {  // line ~1451\n    var span = document.createElement(\"span\");\n    span.innerHTML = msg;                  // line 1464 \u2014 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    \u2192 $_REQUEST[\u0027404ErrorMsg\u0027]\n    \u2192 json_encode()          \u2190 does NOT escape \u003c \u003e /\n    \u2192 avideoAlertInfo()\n    \u2192 avideoAlert()\n    \u2192 avideoAlertHTMLText()\n    \u2192 span.innerHTML = msg   \u2190 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 \u0027avideoAlertInfo(\u0027 . json_encode($_REQUEST[\u0027404ErrorMsg\u0027]) . \u0027);\u0027;\n\n// AFTER (fixed):\necho \u0027avideoAlertInfo(\u0027 . json_encode($_REQUEST[\u0027404ErrorMsg\u0027], JSON_HEX_TAG | JSON_HEX_AMP) . \u0027);\u0027;\n```\n\n`JSON_HEX_TAG` converts `\u003c` \u2192 `\\u003C` and `\u003e` \u2192 `\\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 \u2014 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 \u0027self\u0027; script-src \u0027self\u0027; style-src \u0027self\u0027 \u0027unsafe-inline\u0027\n```\n\n## Impact\n\n- **Session hijacking** \u2014 steal `PHPSESSID` cookie (not HttpOnly by default)\n- **Account takeover** \u2014 use stolen session to change password or email\n- **Phishing** \u2014 inject a realistic login form inside the SweetAlert modal\n- **Worm propagation** \u2014 inject self-spreading payloads via comments/messages\n- **Admin compromise** \u2014 send crafted link to admin, steal session, gain full control",
  "id": "GHSA-wfq5-qgqp-hvhv",
  "modified": "2026-03-20T21:22:24Z",
  "published": "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"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "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",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Unauthenticated Reflected XSS via innerHTML in AVideo"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

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.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
How durations are calculated

Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.

Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.

Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.