Caddy: Unsafe Unicode Handling in FastCGI splitPos Allows Execution of Non-PHP Files
Description
Summary
The FastCGI transport's splitPos() in `modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go` misuses golang.org/x/text/search with search.IgnoreCase when the request path contains a non-ASCII byte. Two distinct flaws in that fallback let an attacker mislead Caddy's FastCGI splitting into treating a non-.php (or other configured split_path extension) file as a script. In any deployment where the attacker can place content into a file served via FastCGI (uploads, file storage, etc.), this can be escalated to remote code execution by crafting a URL whose path triggers either flaw.
This function was adapted from FrankenPHP's code (see the source comment) and inherits the same bugs. Both were originally reported against FrankenPHP by @KC1zs4 as GHSA-3g8v-8r37-cgjm (which absorbed the duplicate GHSA-v4h7-cj44-8fc8). Credit for finding the underlying flaws belongs to @KC1zs4.
Details
var splitSearchNonASCII = search.New(language.Und, search.IgnoreCase)
func (t Transport) splitPos(path string) int {
if len(t.SplitPath) == 0 {
return 0
}
pathLen := len(path)
for _, split := range t.SplitPath {
splitLen := len(split)
for i := range pathLen {
if path[i] >= utf8.RuneSelf {
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
return end
}
break
}
if i+splitLen > pathLen {
continue
}
match := true
for j := range splitLen {
c := path[i+j]
if c >= utf8.RuneSelf {
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
return end
}
break // <-- flaw 1: 'match' is still true
}
if 'A' <= c && c <= 'Z' {
c += 'a' - 'A'
}
if c != split[j] {
match = false
break
}
}
if match {
return i + splitLen
}
}
}
return -1
}
Flaw 1 — Control-flow: stale match after inner non-ASCII fallback
In the inner for j loop, when a byte satisfies c >= utf8.RuneSelf and splitSearchNonASCII.IndexString(...) returns -1, the loop breaks without setting match = false. The outer code then evaluates if match { return i + splitLen } with match still true, returning a position as if the configured extension had been matched. The script-name suffix actually present at that offset is whatever bytes the attacker chose, so a file named name.<U+00A1>.txt gets routed as PHP.
Flaw 2 — Unicode equivalence: search.IgnoreCase folds non-ASCII lookalikes onto ASCII
search.New(language.Und, search.IgnoreCase) performs Unicode equivalence matching (compatibility decomposition + case folding), which goes far beyond the ASCII-only case folding the surrounding code is built for. Many code points fold onto ASCII ., p, h, p, so a path containing ﹒php, .php, .php, .ⓟⓗⓟ, .𝗽𝗵𝗽, .𝓅𝒽𝓅, .𝖕𝖍𝖕, etc. is reported as .php.
Both flaws share the same root cause: invoking search.IgnoreCase to match an ASCII-only, validated-lower-case SplitPath entry against an arbitrary path. Provision() already guarantees every entry is ASCII and lower-cased, so any byte >= utf8.RuneSelf in the path can never be part of a legitimate match — but the fallback ignored that guarantee.
PoC
Run against a Caddy build serving FastCGI to PHP-FPM (or any FastCGI app where script lookup is gated by split_path). Caddyfile:
:8080 {
root * /app/public
php_fastcgi unix//run/php/php-fpm.sock
}
Place attacker-controlled files in /app/public:
/app/public/poc-match-unset.\xc2\xa1.—<?php echo "marker=flaw1\n";/app/public/poc-search-norm.𝗽𝗵𝗽—<?php echo "marker=flaw2\n";
Trigger:
# baseline (correctly NOT routed to PHP)
curl -i --path-as-is "http://127.0.0.1:8080/poc-match-unset.txt/trigger"
curl -i --path-as-is "http://127.0.0.1:8080/poc-search-norm/trigger"
# flaw 1 — the .¡.txt file ends up as SCRIPT_FILENAME
curl -i --path-as-is "http://127.0.0.1:8080/poc-match-unset.%C2%A1.txt/trigger"
# flaw 2 — the .𝗽𝗵𝗽 file ends up as SCRIPT_FILENAME
curl -i --path-as-is "http://127.0.0.1:8080/poc-search-norm.%F0%9D%97%BD%F0%9D%97%B5%F0%9D%97%BD.anything-after-payload.php/trigger"
Both crafted requests respond with the marker payload from the non-.php file, confirming arbitrary code execution through the body of attacker-controlled files.
A standalone reproducer of splitPos() in isolation (no Caddy build needed) is included in GHSA-3g8v-8r37-cgjm; the function in this module is the same logic, so the same payloads apply.
Impact
Comparable to the previous FastCGI split_path issue (GHSA-g966-83w7-6w38 / CVE-2026-24895) but with a stricter precondition: the attacker needs the ability to place content into a file whose name matches one of the bypass patterns (the Unicode lookalike forms or a name containing a non-ASCII byte after a .). Where that precondition holds — common in upload endpoints, user-content stores, package mirrors — the bypass yields RCE in the FastCGI upstream via a single crafted URL, without authentication, over the network.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H — High (8.1).
Patch
Drop the golang.org/x/text/search fallback entirely and treat any byte >= utf8.RuneSelf in the path as a non-match. SplitPath entries are validated ASCII-only and lower-cased upstream, so this preserves correct behavior for every legitimate path while making the Unicode bypasses unrepresentable. The replacement is a tight byte loop with no library calls in the hot path. See fix/fastcgi-splitpos-unicode-bypass (commit 4ddad83c) for the implementation and regression tests.
Credit
Both flaws were originally found and reported by @KC1zs4 against FrankenPHP, where the offending splitPos() function was first introduced before being adapted into this module. The Caddy maintainers thank @KC1zs4 for the high-quality reports.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2026-45135: Caddy's FastCGI splitPos() mishandles non-ASCII bytes, allowing remote code execution via crafted URLs.
Vulnerability
A flaw in the splitPos() function in modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go misuses golang.org/x/text/search with search.IgnoreCase when the request path contains a non-ASCII byte [1][2]. Two distinct control-flow bugs in the non-ASCII fallback let an attacker trick the FastCGI splitting logic into treating a file without a configured split_path extension (e.g., .php) as a script. The vulnerable code was adapted from FrankenPHP and affects all Caddy versions using the FastCGI transport before the fix [3][4].
Exploitation
An attacker must be able to place content into a file served via FastCGI (e.g., through uploads, file storage, or similar) and craft a URL whose path contains a non-ASCII byte that triggers one of the flaws [2][4]. Flaw 1 occurs when a non-ASCII byte is encountered inside the inner matching loop; the code breaks out of the loop without setting match = false, causing a stale match to be returned. Flaw 2 occurs when the top-level fallback splitSearchNonASCII.IndexString finds a match at an incorrect position, returning an offset that corresponds to the attacker-controlled suffix rather than the actual configured split extension [3][4]. No authentication is required if the endpoint is public.
Impact
Successful exploitation allows an attacker to execute arbitrary code on the server with the privileges of the FastCGI process, typically a web application user [2][3]. This can lead to full compromise of the application and server, including data exfiltration, file manipulation, and lateral movement.
Mitigation
Caddy has released a fix in commit xxx (to be confirmed; see GHSA-m675-2p33-xv9g) and advises all users to upgrade to the latest version [3][4]. As a workaround, disable the FastCGI transport or ensure no untrusted users can upload files to paths served via FastCGI until the patch is applied. This CVE is already listed in CISA's Known Exploited Vulnerabilities (KEV) catalog [3].
- GitHub - caddyserver/caddy: Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
- Unsafe Unicode Handling in CGI Path Splitting Allows Execution of Non-PHP Files
- CVE-2026-45135 - GitHub Advisory Database
- Unsafe Unicode Handling in FastCGI splitPos Allows Execution of Non-PHP Files
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1- Range: >= 2.7.0, <= 2.10.2
Patches
0No patches discovered yet.
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
4News mentions
0No linked articles in our index yet.