Improper authorization control for web services In wwbn/avideo
Description
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 ## Summary Type: Authorization-bypass via user-controlled identifier. The Meet plugin's recorded-video upload endpoint (plugin/Meet/uploadRecordedVideo.json.php) authenticates the caller using a single shared Authorization: Bearer <secret> against $objM->secret. Once that check passes, the endpoint reads the target user identifier from the uploaded file's name field, instantiates a User object with that ID, and calls $userObject->login(true, true) — the no-password / encoded-password login path — committing a session for that user and emitting Set-Cookie headers to the caller. There is no check that the caller actually owns the requested users_id. File: plugin/Meet/uploadRecordedVideo.json.php, lines 56-65; secondary in objects/user.php User::login() (no-password branch at lines 1276-1310). Root cause: the upload handler's identity model is "service-to-service" (a Meet/Jitsi recorder posts a finished recording back to AVideo with the shared secret) but the users_id to credit the upload to is parsed from the FILENAME the same caller controls — $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. php 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'; } File: objects/user.php, lines 1249-1329 (User::login() no-password branch). php public function login($noPass = false, $encodedPass = false, $ignoreEmailVerification = false) { // ... 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; } } Why it's wrong: the endpoint conflates two distinct authentication concerns. The shared-secret check answers "is this request coming from a trusted Meet recorder?" but the filename parse answers "which user does this recording belong to?" — and the second answer is taken from the same untrusted caller. Once User->login(true, true) runs, the server has no way to distinguish a legitimate Meet integration from an attacker who happens to know the same secret. The decision to expose this as a session (cookie + _session_regenerate_id) rather than as a one-shot in-process credit makes the impact larger than it needs to be: even if the Meet integration only needed to credit the recording to a user, the implementation gives the caller a fully-authenticated session as that user. ## Exploit Chain 1. Attacker obtains the Meet shared secret. Two plausible paths: - Path A (computational): the secret is md5($global['systemRootPath'] . $global['salt'] . "meet") (plugin/Meet/Meet.php:73). Both inputs sit in videos/configuration.php. AVideo's history of LFI/path-traversal CVEs in this surface (e.g., the import.json.php and listFiles.json.php advisories already accepted on this program) means the salt is a realistic disclosure target. - Path B (timing oracle): plugin/Meet/checkToken.json.php line 26 does if ($objM->secret === $_GET['secret']) with no constant-time comparison and a clear yes/no response body. PHP's === for strings short-circuits on first byte mismatch, so an attacker on the same network segment can recover the 32-hex secret byte-by-byte over the network with timing analysis. Slower than path A but doesn't depend on a separate vulnerability. 2. 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>; .... - Subsequent code runs the encoder pipeline as admin — but the attacker's primary goal was already achieved when the session was established. 4. Attacker captures the Set-Cookie: PHPSESSID=... header from the response and uses that cookie on all subsequent requests. Server treats them as user 1 (admin) — full UI access, all admin endpoints, all video management, plugin configuration, user impersonation, etc. 5. Final state: admin account takeover. The original Meet recorder's flow (legitimate uploads with users_id = the user who scheduled the meeting) is indistinguishable on the wire from the attack flow (users_id = whoever the attacker wants to be). ## Security Impact Severity: sec-high. End state is full account takeover of any user (including admin), reachable from a single HTTP POST once the secret is known. The shared-secret precondition raises AC to High but does not eliminate it as a credible threat — the secret is computable from
Mitigation
Update Impact
Minimal update. May introduce new vulnerabilities or breaking changes.
Ecosystem | Package | Affected version |
|---|---|---|
packagist |
Aliases
References