CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-24895
8.9

FrankenPHP Path Confusion: When 'Ⱥ' Becomes 'ⱥ' and Your Server Explodes

Amit Schendel
Amit Schendel
Senior Security Researcher

Feb 12, 2026·6 min read·12 visits

PoC Available

Executive Summary (TL;DR)

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.

The Hook: Frankenstein's Monster

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 Flaw: The Unicode expansion Trap

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.

The Code: The Smoking Gun

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:])
        // ...
    }
}

The Exploit: Shifting Reality

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:

  1. Attacker uploads shell.txt containing <?php system($_GET['cmd']); ?> to /uploads/.
  2. The server usually prevents executing .txt files as PHP.

The Attack: The attacker requests: GET /ȺȺȺȺshell.php.txt.php

Here is the math of the madness:

  • Original String: /ȺȺȺȺshell.php.txt.php
    • Prefix ȺȺȺȺ: 4 chars * 2 bytes = 8 bytes.
  • Lowercased String: /ⱥⱥⱥⱥshell.php.txt.php
    • Prefix ⱥⱥⱥⱥ: 4 chars * 3 bytes = 12 bytes.
    • Difference: +4 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.

The Impact: Why You Should Care

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:

  1. Execute Code: Run arbitrary commands on the server.
  2. Bypass Access Controls: Access files that should be protected by extension allowlists.
  3. Lateral Movement: Use the compromised server to attack internal networks (database, Redis, etc.).

It effectively turns any file upload feature—even one that strictly validates extensions like .png or .txt—into a backdoor.

The Fix: Stop The Bleeding

If you are running FrankenPHP, check your version immediately. If it is less than 1.11.2, you are vulnerable.

Remediation:

  1. Upgrade: Pull the latest Docker image or binary (v1.11.2 or later).
    • docker pull dunglas/frankenphp
  2. WAF Rules: If you cannot patch immediately, configure your WAF to block requests containing the character 'Ⱥ' (%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.

Official Patches

FrankenPHPGitHub Commit: Fix path splitting logic
FrankenPHPRelease v1.11.2

Fix Analysis (1)

Technical Appendix

CVSS Score
8.9/ 10
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

Affected Systems

FrankenPHP < 1.11.2

Affected Versions Detail

Product
Affected Versions
Fixed Version
FrankenPHP
php
< 1.11.21.11.2
AttributeDetail
CWE IDCWE-180
Attack VectorNetwork
CVSS Score8.9 (High)
ImpactRemote Code Execution (RCE)
Exploit StatusPoC Available
Bug ClassPath Confusion / Byte Index Misalignment

MITRE ATT&CK Mapping

T1190Exploit Public-Facing Application
Initial Access
T1036Masquerading
Defense Evasion
CWE-180
Incorrect Behavior Order: Validate Before Canonicalize

Incorrect Behavior Order: Validate Before Canonicalize

Known Exploits & Detection

GitHub AdvisoryProof of concept URI using 'Ⱥ' expansion.

Vulnerability Timeline

Fix commit authored by Kévin Dunglas
2026-01-26
CVE-2026-24895 Published
2026-02-12
FrankenPHP v1.11.2 Released
2026-02-12

References & Sources

  • [1]GHSA-g966-83w7-6w38
  • [2]CWE-180

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.