CVE-2026-45135: Caddy: Unsafe Unicode Handling in FastCGI splitPos Allows Execution of Non-PHP Files

Published May 18, 2026
·
Updated

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 splitpath 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

go 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 splitpath). Caddyfile:

text :8080 { root /app/public phpfastcgi 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:

bash 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 SCRIPTFILENAME 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 SCRIPTFILENAME 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 splitpath 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.

Other sources

Caddy is an extensible server platform that uses TLS by default. From 2.7.0 until 2.11.3, 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 splitpath 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 vulnerability is fixed in 2.11.3.

MITRE

Affected Software

2 affected componentsFixes available
go/github.com/caddyserver/caddy/v2>=2.7.0<=2.10.2
2.11.3
caddyserver Caddy>=2.7.0<2.11.3

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade go/github.com/caddyserver/caddy/v2 to a version that resolves this vulnerability.

    Fixed in 2.11.3
  2. Upgrade

    Upgrade Caddy (FastCGI reverse proxy splitPos) to a version that resolves this vulnerability.

    Fixed in 2.11.3
  3. Configuration

    Drop the `golang.org/x/text/search` fallback entirely; in `splitPos()` use a tight byte loop so that if any path byte is `>= utf8.RuneSelf`, that path is treated as a non-match (i.e., do not attempt Unicode equivalence matching via `search.New(language.Und, search.IgnoreCase)`).

    Caddy FastCGI transport (modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go) splitPos unicode fallback using golang.org/x/text/search with search.IgnoreCase = Remove the search fallback entirely and treat any byte >= utf8.RuneSelf in the path as a non-match

Event History

May 18, 2026
Advisory Published
via GitHub·01:40 PM
Data Sourced
via GitHub·01:40 PM
DescriptionSeverityWeaknessAffected Software
Jun 23, 2026
CVE Published
via MITRE·05:56 PM
Data Sourced
via MITRE·05:56 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·06:17 PM
DescriptionSeverityWeaknessAffected Software
Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

What is the severity of CVE-2026-45135?

CVE-2026-45135 is considered to have a moderate severity level as it can lead to unexpected behavior in request path handling.

2

How do I fix CVE-2026-45135?

To fix CVE-2026-45135, upgrade to Caddy version 2.11.3 or later.

3

Which versions are affected by CVE-2026-45135?

CVE-2026-45135 affects Caddy versions from 2.7.0 to 2.10.2.

4

What is the impact of CVE-2026-45135?

The impact of CVE-2026-45135 is potential misrouting of requests, which could affect application behavior.

5

Who is affected by CVE-2026-45135?

Users of Caddy versions between 2.7.0 and 2.10.2 are affected by CVE-2026-45135.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203