CVE-2026-27590 — v2
CVE-2026-27590 is a Improper Input Validation vulnerability in github.com/caddyserver/caddy/v2. A fix is available for github.com/caddyserver/caddy/v2 — see the affected versions and patch details below.
Caddy: Unicode case-folding length expansion causes incorrect split_path index (SCRIPT_NAME/PATH_INFO confusion) in FastCGI transport
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
- A successful exploit gives an attacker total control of the affected component, not partial access.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-27590.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
Real-World Exposure
github.com/caddyserver/caddy/v2Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.
Description
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):
- Save this as
poc.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])
}
- Run it:
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 Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐹Go | github.com/caddyserver/caddy/v2 | all versions | 2.11.1go get github.com/caddyserver/caddy/v2@v2.11.1 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/caddyserver/caddy/v2, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update github.com/caddyserver/caddy/v2 to 2.11.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-27590 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2026-27590 can be triaged on real exposure rather than presence alone.
Tailored to CVE-2026-27590. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is CVE-2026-27590 in your dependencies?
O3 Security finds CVE-2026-27590 across Go dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.