Skip to main content

Caddy EUVDEUVD-2026-38560

| CVE-2026-45135 HIGH
Improper Input Validation (CWE-20)
2026-05-18 https://github.com/caddyserver/caddy GHSA-m675-2p33-xv9g
8.1
CVSS 3.1 · Vendor: https://github.com/caddyserver/caddy
Share

Severity by source

Vendor (https://github.com/caddyserver/caddy) PRIMARY
8.1 HIGH
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
Attack Vector
Network
Attack Complexity
High
Privileges Required
None
User Interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

Lifecycle Timeline

2
Source Code Evidence Fetched
May 18, 2026 - 14:30 vuln.today
Analysis Generated
May 18, 2026 - 14:30 vuln.today

DescriptionCVE.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

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

text
: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:

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

More in PHP

View all
CVE-2012-1823 CRITICAL POC
9.8 May 11

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

CVE-2016-1555 CRITICAL POC
9.8 Apr 21

(1) boardData102.php, (2) boardData103.php, (3) boardDataJP.php, (4) boardDataNA.php, and (5) boardDataWW.php in Netgear

CVE-2024-11680 CRITICAL POC
9.8 Nov 26

ProjectSend versions prior to r1720 are affected by an improper authentication vulnerability. Rated critical severity (C

CVE-2025-49113 CRITICAL POC
9.9 Jun 02

Roundcube Webmail contains a critical PHP object deserialization vulnerability (CVE-2025-49113, CVSS 9.9) that allows au

CVE-2017-9841 CRITICAL POC
9.8 Jun 27

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

CVE-2025-0108 HIGH POC
8.8 Feb 12

Palo Alto Networks PAN-OS management web interface contains an authentication bypass allowing unauthenticated attackers

CVE-2021-25298 HIGH POC
8.8 Feb 15

Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re

CVE-2021-25296 HIGH POC
8.8 Feb 15

Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re

CVE-2013-4983 CRITICAL POC
10.0 Sep 10

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

CVE-2023-6553 CRITICAL POC
9.8 Dec 15

The Backup Migration plugin for WordPress is vulnerable to Remote Code Execution in all versions up to, and including, 1

CVE-2024-46506 CRITICAL POC
10.0 May 13

NetAlertX (formerly PiAlert) versions 23.01.14 through 24.x before 24.10.12 allow unauthenticated command injection thro

CVE-2024-8353 CRITICAL POC
9.8 Sep 28

The GiveWP - Donation Plugin and Fundraising Platform plugin for WordPress is vulnerable to PHP Object Injection in all

Share

EUVD-2026-38560 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy