Feb 12, 2026·6 min read·68 visits
FrankenPHP < 1.11.2 mishandles Unicode case conversion in URLs. Because some characters change byte length when lowercased (e.g., 'Ⱥ' -> 'ⱥ'), the server miscalculates where the file path ends. Attackers can use this to execute arbitrary files (RCE). Update to 1.11.2 immediately.
A critical path confusion vulnerability in FrankenPHP's CGI handler allows attackers to execute arbitrary files as PHP scripts. The flaw stems from a mismatch in byte length when converting Unicode characters to lowercase, causing the server to miscalculate the boundary between the script path and path info. By injecting specific characters like 'Ⱥ', attackers can offset the internal pointer and trick the server into executing malicious uploads (e.g., images or text files) as code.
FrankenPHP is a modern, high-performance application server for PHP, built on top of the Caddy web server and written in Go. It’s a beautiful amalgamation of technologies designed to modernize the PHP ecosystem. But as with any complex machinery, the devil is in the details—specifically, in how it handles the Common Gateway Interface (CGI) logic.
To serve a PHP request, the server needs to know two things: which file is the script (SCRIPT_FILENAME) and what comes after it (PATH_INFO). This is usually determined by splitting the request URL at the file extension (e.g., .php). It sounds simple, like cutting a sandwich in half.
However, in CVE-2026-24895, we discover that FrankenPHP was using a rusty knife. By assuming that a string's length remains constant when converted to lowercase, the developers inadvertently opened a door for attackers to slide malicious payloads right past the bouncer. This isn't just a parsing error; it's a logic flaw that turns a harmless file upload mechanism into a full-blown Remote Code Execution (RCE) vector.
The root cause of this vulnerability is a classic "byte vs. character" confusion in Go. The vulnerable code attempted to find the file extension in a URL by first converting the entire path to lowercase and then searching for the extension index. The fatal flaw? Case folding in UTF-8 is not length-invariant.
Consider the character Latin Capital Letter A with Stroke ('Ⱥ', U+023A). In UTF-8, this character takes up 2 bytes (0xC8 0xBA). However, when you run this through Go's strings.ToLower(), it converts to Latin Small Letter A with Stroke ('ⱥ', U+2C65). Here's the kicker: the lowercase version takes up 3 bytes (0xE2 0xB1 0xA5).
For every 'Ⱥ' in the URL, the lowercased string grows by 1 byte. FrankenPHP calculated the split index based on this longer lowercased string but applied that index to the shorter original string. It’s like measuring a distance on a zoomed-in map and then walking that distance in the real world—you’re going to overshoot your destination.
Let's look at the logic that caused the meltdown. The function splitPos was responsible for finding where the script name ends.
The Vulnerable Logic (Simplified):
// BAD: Lowercase the whole path first
lowerPath := strings.ToLower(path)
// Find ".php" in the lowercased version
index := strings.Index(lowerPath, ".php")
// Use that index to slice the ORIGINAL path
scriptName := path[:index]If path is /Ⱥ.php, lowerPath is /ⱥ.php. The index of .php in the lowercased string is shifted because ⱥ is longer. When that shifted index is applied to the original string, it slices at the wrong byte.
The Fix (Commit 04fdc0c1):
The patch completely removes the global strings.ToLower call. Instead, it iterates through the string byte-by-byte. If it sees ASCII, it does a cheap case-insensitive check. If it sees Unicode (bytes >= utf8.RuneSelf), it switches to the golang.org/x/text/search package, which correctly handles offsets without distorting the source string length.
// GOOD: Iterate and handle ASCII/Unicode separately
for i := 0; i < len(path); i++ {
// ... ASCII checks ...
if c >= utf8.RuneSelf {
// Use a matcher that respects original indices
start, _ := matcher.IndexString(path[i:])
// ...
}
}To exploit this, an attacker needs to perform a "Path Confusion" attack. The goal is to upload a file (e.g., shell.txt containing PHP code) and then access it via a URL that tricks FrankenPHP into executing it as a PHP script.
The Setup:
shell.txt containing <?php system($_GET['cmd']); ?> to /uploads/..txt files as PHP.The Attack:
The attacker requests: GET /ȺȺȺȺshell.php.txt.php
Here is the math of the madness:
/ȺȺȺȺshell.php.txt.php
ȺȺȺȺ: 4 chars * 2 bytes = 8 bytes./ⱥⱥⱥⱥshell.php.txt.php
ⱥⱥⱥⱥ: 4 chars * 3 bytes = 12 bytes.FrankenPHP searches the lowercased string for .php. It finds it at the end. Because of the expansion, the index returned is 4 bytes larger than it would be in the original string. When the server applies this index to the original string, the slice point moves forward by 4 bytes, effectively "skipping" characters.
By carefully aligning the number of 'Ⱥ' characters, the attacker shifts the split point so that SCRIPT_FILENAME resolves to /uploads/shell.txt (or similar), while the routing logic—confused by the extension check—hands it off to the PHP engine. The engine executes the text file, and you have RCE.
This is a High Severity (CVSS 8.9) vulnerability for a reason. It requires no authentication, no special privileges, and no user interaction. It purely relies on the server's configuration allowing file uploads or having existing files that can be misinterpreted.
If you are running FrankenPHP in a Docker container or a standard deployment exposed to the web, an attacker can potentially:
It effectively turns any file upload feature—even one that strictly validates extensions like .png or .txt—into a backdoor.
If you are running FrankenPHP, check your version immediately. If it is less than 1.11.2, you are vulnerable.
Remediation:
v1.11.2 or later).
docker pull dunglas/frankenphp%C8%BA) or other expanding Unicode characters in the URL path. However, be aware that many Unicode characters can trigger expansion or contraction, so a blacklist is fragile.This vulnerability serves as a stark reminder: never rely on string transformations (like lowercasing) for indexing if the transformation isn't length-preserving. Always validate indices against the original source of truth.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:P| Product | Affected Versions | Fixed Version |
|---|---|---|
FrankenPHP php | < 1.11.2 | 1.11.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-180 |
| Attack Vector | Network |
| CVSS Score | 8.9 (High) |
| Impact | Remote Code Execution (RCE) |
| Exploit Status | PoC Available |
| Bug Class | Path Confusion / Byte Index Misalignment |
Incorrect Behavior Order: Validate Before Canonicalize
An authenticated administrator with privileges to manage admin accounts (such as change_serversettings) can execute arbitrary SQL commands via a second-order SQL injection vulnerability. The flaw resides in Froxlor's administrative API endpoints, specifically during the handling of IP address mapping parameters which are stored as serialized arrays and later interpolated without sanitization into active database queries. This vulnerability allows high-privileged administrative attackers to compromise the database. By injecting a payload into administrative profile metadata, an attacker can extract sensitive credentials, manipulate backend settings, or potentially disrupt database integrity. The vulnerability affects all versions of Froxlor prior to 2.3.8.
CVE-2026-54543 is a DNS Resource Record (RR) Injection vulnerability in Froxlor, an open-source server administration control panel. Prior to version 2.3.8, the DomainZones.add API command failed to perform strict sanitization and validation on the user-controlled record (label) and type parameters before serializing them into BIND-compatible zone files. An authenticated customer with DNS zone management permissions can inject control characters, breaking out of the original record context to define unauthorized resource records within managed zones.
CVE-2026-42533 is a critical security vulnerability discovered in NGINX Open Source, NGINX Plus, NGINX Ingress Controller, and related products, referred to as the 'Two-Pass Capture-Clobbering' bug. The flaw is situated within NGINX's internal evaluation engine when handling complex variables, exposing a heap-based buffer overflow and information leak when a configuration chains regular expression-based map directives with numbered capture groups. An unauthenticated remote attacker can exploit this weakness by transmitting crafted HTTP requests to trigger remote code execution or defeat ASLR.
Froxlor prior to version 2.3.8 contains a high-severity architectural flaw where the standalone lib/ajax.php entry point bypasses the centralized request validation in lib/init.php. Unauthenticated remote attackers can leverage Cross-Site Request Forgery (CSRF) to induce authenticated administrators to submit forged requests that modify API key whitelists and expiration dates, potentially yielding persistent, out-of-band administrative control.
An insecure data retrieval flaw in the Froxlor server administration panel API allows authenticated remote attackers to retrieve unredacted bcrypt password hashes and Base32-encoded Time-Based One-Time Password (TOTP) seeds. Affected endpoints include several 'get' and 'listing' handlers for customers, administrators, and FTP accounts. Utilizing these leaked parameters, attackers can crack the password hashes offline and concurrently generate valid second-factor authentication codes to completely bypass access controls.
CVE-2026-70666 is a critical Server-Side Request Forgery (SSRF) vulnerability in Netflix Lemur's ACME certificate management integration. Prior to version 1.9.3, the system allowed authority-role users to bypass initial ACME URL allowlist validations when updating an existing authority. Additionally, the underlying ACME network client blindly parsed and connected to dynamic endpoint URLs supplied in JSON responses from the configured ACME directory, allowing attackers to route arbitrary JWS-signed requests to internal services or cloud metadata endpoints.