CVE-2026-27590: Caddy: Unicode case-folding length expansion causes incorrect split_path index (SCRIPT_NAME/PATH_INFO confusion) in FastCGI transport
### Summary Caddy's FastCGI path splitting logic computes the split index on a lowercased copy of the request path and then uses that byte index to slice the original path. This is unsafe for Unicode because `strings.ToLower()` can change UTF-8 byte length for some characters. As a result, Caddy can derive an incorrect `SCRIPT_NAME`/`SCRIPT_FILENAME` and `PATH_INFO`, potentially causing a request that contains `.php` to execute a different on-disk file than intended (path confusion). In setups where an attacker can control file contents (e.g., upload features), this can lead to unintended PHP execution of non-.php files (potential RCE depending on deployment). ### Details The issue is in `github.com/caddyserver/caddy/modules/caddyhttp/fastcgi.Trasnport.splitPos()` (and the subsequent slicing in `buildEnv()`): ``` lowerPath := strings.ToLower(path) idx := strings.Index(lowerPath, strings.ToLower(split)) return idx + len(split) ``` The returned index is computed in the byte space of lowerPath, but `buildEnv()` applies it to the original path: - `docURI = path[:splitPos]` - `pathInfo = path[splitPos:]` - `scriptName = strings.TrimSuffix(path, fc.pathInfo)` - `scriptFilename = caddyhttp.SanitizedPathJoin(fc.documentRoot, fc.scriptName)` This assumes `lowerPath` and `path` have identical byte lengths and identical byte offsets, which is not true for some Unicode case mappings. Certain characters expand when lowercased (UTF-8 byte length increases), shifting the computed index. This creates a mismatch where `.php` is found in the lowercased string at an offset that does not correspond to the same position in the original string, causing the split point to land later/earlier than intended. ### PoC Create a small Go program that reproduces Caddy's `splitPos()` behavior (compute the `.php` split point on a lowercased path, then use that byte index on the original path): 1. Save this as `poc.go`: ```go package main import ( "fmt" "strings" ) func splitPos(path string, split string) int { lowerPath := strings.ToLower(path) idx := strings.Index(lowerPath, strings.ToLower(split)) if idx < 0 { return -1 } return idx + len(split) } func main() { // U+023A: Ⱥ (UTF-8: C8 BA). Lowercase is ⱥ (UTF-8: E2 B1 A5), longer in bytes. path := "/ȺȺȺȺshell.php.txt.php" split := ".php" pos := splitPos(path, split) fmt.Printf("orig bytes=%d\n", len(path)) fmt.Printf("lower bytes=%d\n", len(strings.ToLower(path))) fmt.Printf("splitPos=%d\n", pos) fmt.Printf("orig[:pos]=%q\n", path[:pos]) fmt.Printf("orig[pos:]=%q\n", path[pos:]) // Expected split: right after the first ".php" in the original string want := strings.Index(path, split) + len(split) fmt.Printf("expected splitPos=%d\n", want) fmt.Printf("expected orig[:]=%q\n", path[:want]) } ``` 2. Run it: ```console go run poc.go ``` Output on my side: ``` orig bytes=26 lower bytes=30 splitPos=22 orig[:pos]="/ȺȺȺȺshell.php.txt" orig[pos:]=".php" expected splitPos=18 expected orig[:]="/ȺȺȺȺshell.php" ``` Expected split is right after the first `.php` (`/ȺȺȺȺshell.php`). Instead, the computed split lands later and cuts the original path after `shell.php.txt`, leaving `.php` as the remainder. ### Impact Security boundary bypass/path confusion in script resolution. In typical deployments, `.php` extension boundaries are relied on to decide what is executed by PHP. This bug can cause Caddy/FPM to execute a different file than intended by confusing `SCRIPT_NAME`/`SCRIPT_FILENAME`. If an attacker can place attacker-controlled content into a file that can be resolved as `SCRIPT_FILENAME` (common in web apps with uploads or writable directories), this can lead to unintended PHP execution of non-.php files and potentially remote code execution. Severity depends on deployment and presence of attacker-controlled file writes, but the primitive itself is remotely triggerable via crafted URLs. This vulnerability was initially reported to FrankenPHP (https://github.com/php/frankenphp/security/advisories/GHSA-g966-83w7-6w38) by @AbdrrahimDahmani. The affected code has been copied/adapted from Caddy, which, according to research, is also affected. The patch is a port of the FrankenPHP patch.
Affected Software
Event History
Frequently Asked Questions
What is the severity of CVE-2026-27590?
CVE-2026-27590 has been categorized as a high-severity vulnerability due to its potential for significant impact on the affected systems.
How do I fix CVE-2026-27590?
To remediate CVE-2026-27590, upgrade Caddy to version 2.11.1 or later to ensure the FastCGI path splitting logic is secure.
What systems are affected by CVE-2026-27590?
CVE-2026-27590 affects all versions of Caddy prior to 2.11.1.
What type of vulnerability is CVE-2026-27590?
CVE-2026-27590 is a path manipulation vulnerability related to improper handling of Unicode input.
Is CVE-2026-27590 related to FastCGI?
Yes, CVE-2026-27590 specifically involves Caddy's FastCGI path splitting logic which is affected by this vulnerability.