Severity by source
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Primary rating from Vendor (https://github.com/caddyserver/caddy) · only source for this CVE.
CVSS VectorVendor: https://github.com/caddyserver/caddy
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Lifecycle Timeline
2DescriptionCVE.org
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.
AnalysisAI
Remote code execution in Caddy web server (versions 2.7.0 through 2.10.2) is possible when the FastCGI reverse proxy's splitPos() function mishandles non-ASCII bytes in request paths, causing non-PHP files to be routed to a FastCGI upstream like PHP-FPM as if they were scripts. Where an attacker can place file content (uploads, user-content stores, package mirrors), a single crafted URL containing Unicode lookalikes for '.php' or a non-ASCII byte after a dot yields unauthenticated RCE. Publicly available exploit code exists (detailed PoC in the GHSA advisory) and the issue inherits two bugs from FrankenPHP's adapted code; no public exploitation has been reported and EPSS data was not provided.
Technical ContextAI
Caddy's modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go uses splitPos() to determine the split point between SCRIPT_FILENAME and PATH_INFO when proxying to a FastCGI backend (typically PHP-FPM). The function was adapted from FrankenPHP and falls back to golang.org/x/text/search with search.IgnoreCase whenever it encounters a byte >= utf8.RuneSelf. Two defects exist: (1) a control-flow bug where the inner loop breaks on a non-ASCII byte without resetting match=false, so the function returns a 'matched' position even though no real extension match occurred; (2) Unicode equivalence/case folding in x/text/search collapses code points such as U+FF50 (p), mathematical bold/italic letters, and full-width or small-form dots onto ASCII '.php', so paths like '.𝗽𝗵𝗽' or '.php' are treated as the configured split extension. Because Provision() already guarantees SplitPath entries are validated ASCII and lower-cased, the Unicode fallback was never necessary. The vulnerability class is CWE-20 (improper input validation), specifically improper handling of Unicode normalization at a security-relevant string comparison. Affected package is pkg:go/github.com/caddyserver/caddy/v2.
RemediationAI
Vendor-released patch: upgrade Caddy to 2.11.3 or later, which drops the golang.org/x/text/search fallback entirely and treats any byte >= utf8.RuneSelf as a non-match (upstream fix commit 4ddad83c on branch fix/fastcgi-splitpos-unicode-bypass), per the advisory at https://github.com/caddyserver/caddy/security/advisories/GHSA-m675-2p33-xv9g. If immediate upgrade is not feasible, mitigate by preventing attacker-controlled files from being written into any directory served via the FastCGI handler - store uploads outside the document root and serve them through an application route rather than directly via php_fastcgi, which removes the precondition needed for RCE; the trade-off is rework of upload-serving logic. As an additional control, enforce strict server-side filename normalization that rejects non-ASCII bytes in stored filenames before persistence, accepting that this may break legitimate internationalized filenames. Blocking requests whose decoded path contains non-ASCII bytes at a front proxy is another option but will break Unicode URL support for the entire site. For environments running FrankenPHP, also track GHSA-3g8v-8r37-cgjm.
sapi/cgi/cgi_main.c in PHP before 5.3.12 and 5.4.x before 5.4.2, when configured as a CGI script (aka php-cgi), does not
(1) boardData102.php, (2) boardData103.php, (3) boardDataJP.php, (4) boardDataNA.php, and (5) boardDataWW.php in Netgear
ProjectSend versions prior to r1720 are affected by an improper authentication vulnerability. Rated critical severity (C
Roundcube Webmail contains a critical PHP object deserialization vulnerability (CVE-2025-49113, CVSS 9.9) that allows au
Util/PHP/eval-stdin.php in PHPUnit before 4.8.28 and 5.x before 5.6.3 allows remote attackers to execute arbitrary PHP c
Palo Alto Networks PAN-OS management web interface contains an authentication bypass allowing unauthenticated attackers
Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re
Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re
The get_referers function in /opt/ws/bin/sblistpack in Sophos Web Appliance before 3.7.9.1 and 3.8 before 3.8.1.1 allows
The Backup Migration plugin for WordPress is vulnerable to Remote Code Execution in all versions up to, and including, 1
NetAlertX (formerly PiAlert) versions 23.01.14 through 24.x before 24.10.12 allow unauthenticated command injection thro
The GiveWP - Donation Plugin and Fundraising Platform plugin for WordPress is vulnerable to PHP Object Injection in all
Same weakness CWE-20 – Improper Input Validation
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-38560
GHSA-m675-2p33-xv9g