Описание
Froxlor has CSRF Vulnerability in AJAX Endpoint — Missing Cross-Site Request Forgery Protection
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 (bypasseslib/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)
Contrast with normal flow: All admin/customer pages (e.g., admin_customers.php, customer_domains.php) do:
Step 2: Ajax Constructor — Session Created, No CSRF Check
Step 3: Ajax::handle() — Session Validation Only, Routes to Action
Step 4: getValidatedSession() — Only Checks Session Exists
Step 5: editApiKey() — Database Mutation Without Origin Validation
Step 6: Evidence from Legitimate Frontend — No CSRF Token Sent Even in Normal Usage
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)
SameSite=Lax Analysis
Froxlor sets session cookie with SameSite=Lax (UI.php:124):
Why SameSite=Lax is NOT a complete mitigation:
-
HTTP deployments: When
requestIsHttps()returns false (plain HTTP), thesecureflag is false. Many browsers (particularly older Safari and Firefox) requireSecurefor strict SameSite enforcement. Froxlor's own documentation supports HTTP deployment for internal networks, making this a realistic scenario. -
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.
-
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.
-
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).
Confirmed Vulnerable Actions in Ajax::handle()
All POST-based actions in the switch statement lack CSRF protection:
| Action | Method | State Change | Risk |
|---|---|---|---|
editapikey | POST | UPDATE api_keys SET allowed_from, valid_until | HIGH |
updatetablelisting | POST | UPDATE panel_usercolumns (user preferences) | Low |
getConfigDetails | POST | Read-only (config parsing) | None |
Impact
- Confidentiality: None — the attacker cannot directly read data through this CSRF vector
- 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). - Availability: Low — the attacker could set
valid_untilto a past timestamp, disabling the API key
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.
Proof of Concept
PoC HTML File
Reproduction Steps
-
Setup:
- Deploy Froxlor 2.3.7 on a test server (e.g.,
http://192.168.1.100/) - Create an administrator account and log in
- Create at least one API key (Settings → API Keys)
- Deploy Froxlor 2.3.7 on a test server (e.g.,
-
Prepare PoC:
- Host the PoC HTML file on a different origin (e.g.,
http://attacker.local/csrf_poc.html) - Note the Froxlor server is on
http://(not HTTPS, common for internal deployments)
- Host the PoC HTML file on a different origin (e.g.,
-
Execute:
- Ensure the Froxlor administrator has an active session
- Open the PoC HTML file in the same browser (different tab)
- The form auto-submits
-
Verify:
- Check the API key in the Froxlor admin panel
- The
allowed_fromfield now contains10.99.99.99 - The
valid_untilfield shows no expiration - Or verify directly:
SELECT id, allowed_from, valid_until FROM api_keys WHERE id=1;
Expected Result
Before attack:
After attack:
Root Cause
The 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.
Architecturally, there are two security enforcement paths:
- Normal pages:
admin_*.php→require lib/init.php→ CSRF check ✅ - AJAX endpoint:
lib/ajax.php→new Ajax()->handle()→ CSRF check ❌
The 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.
Fix Recommendation
Option A (Recommended): Route AJAX Through init.php
Refactor lib/ajax.php to use the standard bootstrap, ensuring all security controls apply uniformly:
Pros: All security controls (CSRF, rate limiting, session management, area enforcement) apply uniformly. No code duplication. Cons: Requires frontend changes to include CSRF token in AJAX requests.
Option B (Minimal): Add CSRF Check to Ajax Class
Add CSRF token validation directly in the Ajax class:
Frontend changes required (for both options):
CSRF Token Available in Twig Templates
The CSRF token is already available as a Twig global variable ({{ csrf_token }}) set in init.php:361. Templates can expose it via:
Пакеты
froxlor/froxlor
<= 2.3.7
2.3.8
Связанные уязвимости
Froxlor is open source server administration software. Prior to 2.3.8, the standalone lib/ajax.php entry point bypasses the centralized request validation in lib/init.php, and Ajax::handle in lib/Froxlor/Ajax/Ajax.php checks only for a valid session before routing state-changing requests. The editapikey action in Ajax::editApiKey updates allowed_from and valid_until without validating a CSRF token, while templates/Froxlor/assets/js/jquery/apikeys.js sends no token because the endpoint does not require one. An unauthenticated attacker can induce an authenticated administrator's browser to submit a forged request that adds an attacker-controlled address to an API key's allowed_from list or removes its expiration, weakening the key's security restrictions. This issue is fixed in version 2.3.8.
Froxlor is open source server administration software. Prior to 2.3.8, ...