GitHub Advisoryadvisory · Packagist
GHSA-xpr4-8vp6-c87j Froxlor has CSRF Vulnerability in AJAX Endpoint — Missing Cross-Site Request Forgery Protection
MEDIUM
Official page ↗
- Aliases
- CVE-2026-55593
- Ecosystem
- Packagist
- Packages
- froxlor/froxlor
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
- Weaknesses
- CWE-352
- Reviewed
- 2026-08-18
Affected packages
• Packagist froxlor/froxlor: < 2.3.8 → fixed in 2.3.8
Description
Summary
The 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).
---
Affected Component
• File: `lib/ajax.php` — the AJAX endpoint entry point (bypasses `lib/init.php`)
• File: `lib/Froxlor/Ajax/Ajax.php:66-92` — `Ajax::handle()` (no CSRF check before routing)
• File: `lib/Froxlor/Ajax/Ajax.php:257-315` — `Ajax::editApiKey()` (writes to database without CSRF check)
• Version: Froxlor 2.3.7 (likely all prior 2.x versions)
---
Complete Call Chain: Entry Point → Vulnerable Code
Step 1: Entry Point — `lib/ajax.php` (standalone bootstrap, bypasses `lib/init.php`)
```php
// lib/ajax.php:26-47
namespace Froxlor;
use Froxlor\Ajax\Ajax;
require_once dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/lib/userdata.inc.php';
require_once dirname(__DIR__) . '/lib/functions.php';
require_once dirname(__DIR__) . '/lib/tables.inc.php';
// CRITICAL: This file does NOT include lib/init.php
// Therefore: NO CSRF token is checked before processing the request
echo (new Ajax)->handle();
```
Contrast with normal flow: All admin/customer pages (e.g., `admin_customers.php`, `customer_domains.php`) do:
```php
const AREA = 'admin';
require __DIR__ . '/lib/init.php'; // action = Request::any('action'); // theme = Request::any('theme', 'Froxlor');
UI::sendHeaders(); // Starts session, sets security headers
UI::sendSslHeaders(); // HSTS headers
// MISSING: CSRF token validation on POST/PUT/PATCH/DELETE
}
```
Step 3: Ajax::handle() — Session Validation Only, Routes to Action
```php
// lib/Froxlor/Ajax/Ajax.php:66-92
public function handle()
{
$this->userinfo = $this->getValidatedSession(); // Only checks: isset($_SESSION['userinfo'])
// MISSING: CSRF token validation before routing
// Comparison: init.php lines 363-369 WOULD check here:
// if (in_array($_SERVER['REQUEST_METHOD'], ['POST', 'PUT', 'PATCH', 'DELETE'])) {
// $current_token = Request::post('csrf_token', ...);
// if ($current_token != CurrentUser::getField('csrf_token')) { ERROR; }
// }
switch ($this->action) {
case 'editapikey':
return $this->editApiKey(); // updateTablelisting(); // $keyid,
'af' => $allowed_from, // Attacker's IP written here
'vu' => $valid_until_db, // -1 = never expires
'aid' => $this->userinfo['adminid'],
'cid' => $cid
]);
return $this->jsonResponse(['allowed_from' => $allowed_from, 'valid_until' => $valid_until]);
}
```
Step 6: Evidence from Legitimate Frontend — No CSRF Token Sent Even in Normal Usage
```javascript
// templates/Froxlor/assets/js/jquery/apikeys.js:9-17
// Even the legitimate frontend does NOT send a csrf_token:
$.ajax({
url: "lib/ajax.php?action=editapikey",
type: "POST",
dataType: "json",
data: {
id: akid,
allowed_from: _this.val(),
valid_until: $('div[data-entry="' + akid + '"] #valid_until').val()
// NOTE: No csrf_token field here — the backend doesn't require it
},
// ...
});
```
This confirms: the backend does not validate CSRF tokens, so the frontend code does not bother sending one.
---
CSRF Protection Gap: Side-by-Side Comparison
| Aspect | `lib/init.php` (Normal Pages) | `lib/ajax.php` (AJAX Endpoint) |
|--------|------------------------------|-------------------------------|
| Includes init.php | Yes (all admin_*.php, customer_*.php) | No — standalone bootstrap |
| Session validation | ✅ `CurrentUser::hasSession()` | ✅ `CurrentUser::hasSession()` |
| CSRF token generation | ✅ `Froxlor::genSessionId(20)` | ❌ Not generated |
| CSRF token check (POST/PUT/PATCH/DELETE) | ✅ Lines 363-369 | ❌ Missing entirely |
| Rate limiting | ✅ `RateLimiter::run()` | ❌ Not called |
| Area enforcement | ✅ Admin/Customer area check | ❌ Not enforced |
---
Vulnerability Verification
Attack Path (Complete)
```
[Attacker] Hosts malicious HTML page at https://attacker.com/csrf.html
document.getElementById('csrf').submit();
│
▼
[Victim] Froxlor administrator browses to https://attacker.com/csrf.html
• Victim has an active session at https://froxlor.example.com
• Session cookie: PHPSESSID=, SameSite=Lax
│
▼
[Browser] Auto-submits POST to https://froxlor.example.com/lib/ajax.php?action=editapikey
• Cookie behavior depends on SameSite policy (see below)
│
▼
[Server: lib/ajax.php]
→ require userdata.inc.php, functions.php, tables.inc.php
→ (new Ajax)->handle()
│
▼
[Server: Ajax::__construct()] (Ajax.php:54-61)
→ $this->action = 'editapikey' (from GET query string)
→ UI::sendHeaders() → session_start()
→ NO CSRF CHECK
│
▼
[Server: Ajax::handle()] (Ajax.php:66-68)
→ getValidatedSession() → CurrentUser::hasSession() → TRUE
(session cookie was sent with request)
→ NO CSRF CHECK before routing
│
▼
[Server: Ajax::editApiKey()] (Ajax.php:257-315)
→ $keyid = 1 (from POST)
→ $allowed_from = 'ATTACKER_IP' (from POST)
→ $valid_until_db = -1 (from POST, parsed)
→ UPDATE api_keys SET allowed_from='ATTACKER_IP', valid_until=-1 WHERE id=1
│
▼
[Impact] API key #1 now allows connections from ATTACKER_IP, never expires
```
SameSite=Lax Analysis
Froxlor sets session cookie with `SameSite=Lax` …
Related
Alias · CVE (1)
- CVE-2026-55593 Froxlor is open source server administration software. vulnerability
Weakness · CWE (1)
- CWE-352 Cross-Site Request Forgery weakness