Feb 24, 2026·7 min read·89 visits
Caddy and FrankenPHP contain a 'Path Confusion' vulnerability due to unsafe Unicode lowercasing in the FastCGI module. An attacker can upload a file with a safe extension (e.g., .png) and request it with a specially crafted URL containing Unicode characters (like 'ẞ'). The server miscalculates the file path, executing the image as a PHP script. Immediate patching to v2.11.1 is required.
A critical logic flaw in Caddy's FastCGI transport layer allows for Remote Code Execution (RCE) via Unicode case-folding collisions. By exploiting how Go's `strings.ToLower` handles specific Unicode characters, attackers can desynchronize path parsing, tricking the server into executing arbitrary files (like images) as PHP scripts. This affects Caddy and the popular FrankenPHP application server.
We often laud Go for its memory safety. It saves us from the buffer overflows and dangling pointers that plague C and C++. But memory safety doesn't save you from logic safety, and it certainly doesn't save you from the absolute nightmare that is Unicode handling.
Enter CVE-2026-27590. This isn't a complex heap feng-shui attack. It's a classic case of "the developer assumed X, but the spec says Y." In this instance, the affected component is the FastCGI transport in Caddy (and by extension, FrankenPHP). FastCGI is the protocol that allows web servers to talk to application backends like PHP-FPM.
To make this communication work, the server needs to split a request URL—say /blog/index.php/article/1—into two parts: the script to execute (/blog/index.php) and the path info to pass to that script (/article/1). Caddy does this by looking for the script extension (usually .php). To be helpful and user-friendly, Caddy tries to find this extension case-insensitively. And that is exactly where the wheels fall off.
The root cause of this vulnerability is a discrepancy between the length of a string in its original form and its length after being lowercased. In ASCII, A and a are both 1 byte. The world is simple. But in the Unicode standard, this guarantee vanishes.
Consider the character ẞ (Latin Capital Letter Sharp S, \u1E9E). In UTF-8, this character requires 3 bytes. When you run Go's strings.ToLower() on it, it transforms into ß (Latin Small Letter Sharp S, \u00DF), which requires only 2 bytes.
The Logical Fallacy: Caddy's logic proceeded in three fatal steps:
.php..php inside this lowercased string.Because the lowercasing operation changed the byte length of the string (shrinking or expanding it depending on the characters used), the index found in step 2 is invalid for the string in step 3. It points to the wrong byte offset. It's like measuring a distance on a map, folding the map in half, and then trying to use that same measurement on the real terrain. You're going to end up in the wrong place.
Let's look at a reconstruction of the vulnerable logic to understand exactly what happened. This is a pattern I call "Index Confusion."
// Vulnerable Logic Concept
func splitFastCGIPath(path string) (script, info string) {
// 1. Create a lowercased copy to find extensions case-insensitively
lowerPath := strings.ToLower(path)
// 2. Find where ".php" starts in the lowercased version
splitIndex := strings.Index(lowerPath, ".php")
if splitIndex == -1 {
return path, ""
}
// 3. CRITICAL FAIL: Using the index from 'lowerPath' on 'path'
// If len(lowerPath) != len(path), this cut is garbage.
scriptName := path[:splitIndex+4] // +4 for ".php"
pathInfo := path[splitIndex+4:]
return scriptName, pathInfo
}The Patch: The fix in Caddy v2.11.1 is straightforward but essential: do not mix indices across different string representations. If you need to search case-insensitively, you must track the byte offsets in the original string, or perform the check on the original string using a custom case-insensitive comparison that doesn't alter the buffer length.
> [!NOTE]
> Developer Lesson: If you mutate a string (lowercase, normalize, trim), all previous indices into that string are arguably void. Never apply an index from Mutation(A) back to A unless you can mathematically guarantee 1:1 byte mapping.
How do we weaponize a byte offset mismatch? We use it to trick the server into agreeing to execute a file it normally wouldn't. The goal is to upload a malicious payload (hidden in an image) and force Caddy to feed it to the PHP interpreter.
Step 1: The Setup
We upload a file named avatar.png. Inside this valid PNG image, we embed a PHP webshell in the metadata comments:
<?php system($_GET['cmd']); ?>.
Step 2: The Attack Vector We need to construct a URL that meets two criteria:
.php (so Caddy triggers the FastCGI transport).ẞ (or similar) to trigger the index drift.Request: GET /uploads/avatar.png/fooẞ/bar.php
Step 3: The Execution Let's trace the execution flow:
/uploads/avatar.png/foo + ẞ (3 bytes) + /bar.php/uploads/avatar.png/foo + ß (2 bytes) + /bar.phpIn the Lower Path, the string has shrunk by 1 byte (3 bytes became 2). The .php extension appears 1 byte earlier than it does in the Original Path.
When Caddy calculates the split index based on Lower Path, it gets index N. When it slices Original Path[:N], it cuts the string 1 byte short of the actual .php location (relative to the content). Depending on the exact positioning and padding, this allows the attacker to manipulate exactly where the SCRIPT_FILENAME ends.
In a successful exploit scenario, the desynchronization causes Caddy to sanitize the path incorrectly or identify the split point such that the PATH_INFO is swallowed, and the SCRIPT_FILENAME passed to the backend becomes /uploads/avatar.png. The PHP-FPM backend, blindly trusting the web server, executes the PNG as PHP. Boom. RCE.
This vulnerability scores an 8.9 (High) on the CVSS scale for good reason. It is an unauthenticated Remote Code Execution flaw. If you are running Caddy with PHP (or FrankenPHP) and you allow users to upload files—avatars, documents, attachments—you are likely vulnerable.
The Blast Radius:
*.php in the URL might miss this if they aren't normalizing Unicode exactly the same way Go does. The request looks like a path traversing past an image, which is often valid behavior for image processing scripts.Once code execution is achieved, the attacker has the privileges of the www-data user (or whoever runs Caddy). From there, they can read database credentials, modify source code, or pivot to the internal network. It is a game-over scenario.
Remediation is binary: you are either patched or you are exposed.
1. Update Immediately
2. Mitigation Strategies (If you can't update)
If you are stuck on an old version (why?), you can mitigate this by strictly validating request paths at the reverse proxy level before they hit Caddy, blocking any request containing the sequence .php that also contains high-byte Unicode characters. However, this is brittle.
Another robust defense is ensuring that your upload directories are outside the web root, or explicitly instructing the web server to never execute PHP files in upload directories, regardless of what the FastCGI transport thinks. In Caddy, this can be done with a respond directive for specific paths.
# Caddyfile Mitigation Example
@uploads {
path /uploads/*
}
respond @uploads 403This simple block ensures that even if the path confusion occurs, the server refuses to serve the request from that directory.
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 |
|---|---|---|
Caddy CaddyServer | < 2.11.1 | 2.11.1 |
FrankenPHP Dunglas | All versions using Caddy < 2.11.1 | Latest |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-180 (Incorrect Behavior Order) |
| CVSS v4.0 | 8.9 (High) |
| Attack Vector | Network |
| Attack Complexity | Low |
| Privileges Required | None |
| Exploit Status | Proof-of-Concept Available |
The software performs an operation (lowercasing) that changes the length of the data, but uses the resulting indices to process the original data, leading to incorrect parsing.
The self-hosted HTTP transport mode of @arikusi/deepseek-mcp-server (an MCP server for DeepSeek V4) exposes its JSON-RPC endpoint (POST /mcp) without authentication in versions 1.4.2 through 1.7.0. Unauthenticated clients can establish Model Context Protocol sessions and invoke tools, consuming the host's configured DeepSeek API key.
A Server-Side Template Injection (SSTI) leading to Remote Code Execution (RCE) was discovered in the mcp-contextforge-gateway package before version 1.0.0. The vulnerability stems from an unsandboxed Jinja2 template rendering environment combined with an unsafe fallback mechanism using Python's native str.format() function. Attackers with template modification access could bypass static regex filters to execute arbitrary commands on the hosting platform.
CVE-2026-55596 is a critical DOM-based Cross-Site Scripting (XSS) vulnerability in the Plate rich-text editor framework (specifically within the @platejs/media package). The issue stems from an optimization fast-path that short-circuits safety parsing if a provider or source URL is already declared on an element. Consequently, serialized documents carrying malicious javascript: URLs bypass protocol sanitization and are loaded directly into iframe elements, leading to code execution.
The npm package 'pickem' is vulnerable to a terminal escape-sequence injection (CWE-150). Unsanitized terminal outputs allow attackers to execute arbitrary shell commands via clipboard hijacking (OSC 52) or manipulate terminal displays through Control Sequence Introducers (CSI).
CVE-2026-55537 is a server-side request forgery (SSRF) and time-of-check time-of-use (TOCTOU) vulnerability in the PraisonAI multi-agent framework before version 4.6.58. The flaw exists in the job-submission component's webhook URL validation logic. When DNS resolution fails during verification, the application fails open, enabling attackers to register unresolvable URLs. When a completed job triggers the webhook, the application performs a fresh DNS resolution that attackers can manipulate to target internal resources.
Prior to version 5.0.8, django CMS fails to respect dynamically declared Vary HTTP headers in its internal page cache. This allows remote attackers to bypass authorization, leak sensitive information across user sessions, or poison the page cache by sending requests with custom headers.