# AVideo's Meet plugin: `uploadRecordedVideo.json.php` derives `users_id` from the uploaded filename and calls passwordless `User->login()`, allowing any caller with the Meet shared secret to obtain a session as arbitrary users including admin

AVideo Meet plugin's uploadRecordedVideo endpoint parses users_id from the uploaded filename and calls passwordless User->login(), letting anyone with the Meet secret take over any account including admin.

- Payout: Research
- Severity: high
- Program: AVideo
- Found by: offset
- Disclosed: 2026-05-15
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-qxvm-r42f-5p8j
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-qxvm-r42f-5p8j
- Tags: area:web domain:web impact:ato impact:privesc kind:research platform:ghsa sev:high tech:php technique:chained technique:source-review vuln:access-control vuln:auth-bypass

**Target:** plugin/Meet/uploadRecordedVideo.json.php upload endpoint
**Root cause:** The endpoint authenticates the caller with a single shared Meet secret but derives the target users_id from the attacker-controlled uploaded filename, then calls User->login(true, true) which skips password verification and commits a session for that user.
**Steps:**
1. Obtain the Meet shared secret: either compute `md5($global['systemRootPath'] . $global['salt'] . "meet")` from a leaked videos/configuration.php, or recover it byte-by-byte via timing on `checkToken.json.php`'s `===` comparison.
2. POST to `/plugin/Meet/uploadRecordedVideo.json.php` with header `Authorization: Bearer <Meet secret>` and a multipart file field `upl` whose filename is `1-anything.mp4` (leading digits = target users_id).
3. Server parses `$users_id = explode('-', $_FILES['upl']['name'])[0]` → `1`, runs `new User(1); $userObject->login(true, true);` setting `$_SESSION['user']` and issuing `Set-Cookie: PHPSESSID=<new-sid>`.
4. Capture the `Set-Cookie: PHPSESSID=...` from the response and reuse it on subsequent requests to act as the targeted user (admin).
**How found:** Source review of the Meet plugin upload handler: noticed the identity model conflates service-to-service shared-secret auth with per-user identity taken from the same caller-controlled filename, then traced User::login(true, true) to confirm it sets session and cookie without a password check.
**Exploitation:** Single HTTP POST with the shared secret and a crafted filename yields a fully authenticated session cookie for any chosen users_id, including admin (id 1 on default installs), granting full UI/API admin access for the session lifetime.
**Escalation:** Secret is deterministically computable from videos/configuration.php (reachable via prior path-traversal CVEs in the same surface), and checkToken.json.php provides a network timing oracle to recover it without any other bug.
**Insight:** When a service-to-service endpoint both authenticates the caller and derives a per-user identity, check whether the identity comes from a separate trusted source (signed claim) or from the same caller-controlled input — filenames, headers, or body fields are common smuggling points for users_id. Also grep for `login(true` / no-password login calls reachable from non-login endpoints.

## Proof of concept (copied exactly from the original report)

**Vulnerable upload handler parsing users_id from filename**

```
$users_id = explode('-', $_FILES['upl']['name'])[0];`. There is no signed claim, no separate proof-of-identity, no allowlist. The subsequent `$userObject->login(true, true)` call invokes the no-password login path which sets `$_SESSION['user']`, calls `setUserCookie(...)`, and `_session_regenerate_id()` — exactly the operations a normal login performs. The response carries the new `PHPSESSID` back to the caller, who can then reuse it on every subsequent request to act as the targeted user. The Meet shared secret is `md5($global['systemRootPath'] . $global['salt'] . "meet")` (`Meet.php:73`), so any attacker who can read `videos/configuration.php` (e.g., via a path-traversal CVE such as `GHSA-83xq-8jxj-4rxm` or `GHSA-4wmm-6qxj-fpj4` that the project has already addressed in this surface area) can compute the Meet secret deterministically and pivot to full account takeover.

## Affected Code

**File:** `plugin/Meet/uploadRecordedVideo.json.php`, lines 33-73.

if (empty($token)) {
    forbiddenPage('Token not found');
}

$objM = AVideoPlugin::getObjectDataIfEnabled("Meet");
if (empty($objM)) {
    forbiddenPage('Plugin disabled');
}

if ($objM->secret != $token) {                              // <-- shared-secret auth, no per-user proof
    forbiddenPage('Token does not match');
}

if (empty($_FILES['upl'])) {
    forbiddenPage('videoFile not found');
}

$users_id = explode('-', $_FILES['upl']['name'])[0];        // <-- BUG: target users_id parsed from attacker-controlled filename

$userObject = new User($users_id);
$userObject->login(true, true);                             // <-- BUG: passwordless login as the chosen user; sets $_SESSION + Set-Cookie
$tmpFile = getTmpDir() . uniqid();

if (move_uploaded_file($_FILES['upl']['tmp_name'], $tmpFile)) {
    $_FILES['upl']['tmp_name'] = $tmpFile;
    require $global['systemRootPath'] . 'objects/aVideoQueueEncoder.json.php';
```

**Passwordless login branch setting session and cookie**

```
if ($noPass) {
        $user = $this->find($this->user, false, true);      // <-- no password check
    }
    // ...
    } elseif ($user) {
        $_SESSION['user'] = $user;                          // <-- session set for the impersonated user
        $this->setLastLogin($_SESSION['user']['id']);
        // ...
        self::setUserCookie($rememberme, $user['id'], $user['user'], $passhash, $expires);
        AVideoPlugin::onUserSignIn($_SESSION['user']['id']);
        $_SESSION['loginAttempts'] = 0;
        _session_regenerate_id();                           // <-- new SID committed in Set-Cookie response
        _session_write_close();
        return self::USER_LOGGED;
```

**Exploit POST with filename 1-anything.mp4**

```
Attacker prepares an HTTP POST to `/plugin/Meet/uploadRecordedVideo.json.php`:
   - `Authorization: Bearer <Meet secret>`
   - Multipart body with one file field named `upl`. The filename is set to `1-anything.mp4` (where `1` is the `users_id` of the admin or any target user — the format is `<users_id>-<arbitrary>`). The file body itself can be anything that survives the surrounding aVideoQueueEncoder pipeline (an empty file is enough to reach the login call before the encoder rejects).
3. Server flow:
   - Line 33: token present, ok.
   - Line 46: `$objM->secret != $token` → false (matches), passes.
   - Line 51: `$_FILES['upl']` present, ok.
   - Line 56: `$users_id = explode('-', '1-anything.mp4')[0]` → `'1'`.
   - Line 59-60: `$userObject = new User(1); $userObject->login(true, true);` — passwordless login as user 1 (admin). `$_SESSION['user']` is set, `setUserCookie` runs, `_session_regenerate_id` issues a new session ID, and the response carries `Set-Cookie: PHPSESSID=<new-sid>; ...`.
```

