GitHub Security Advisory: GHSA-g7pc-pc7g-h8jh
Package: github.com/caddyserver/caddy/v2 (GO)
Severity: HIGH (CVSS 0.0)
CWE: CWE-178: Improper Handling of Case Sensitivity
Vulnerable versions: < 2.11.1
Patched in: 2.11.1

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

Details:
### Summary
Caddy's HTTP `path` request matcher is intended to be case-insensitive, but when the match pattern contains percent-escape sequences (`%xx`) it compares against the request's escaped path without lowercasing. An attacker can bypass path-based routing and any access controls attached to that route by changing the casing of the request path.

### Details
In Caddy `v2.10.2`, `MatchPath` is explicitly designed to be case-insensitive and lowercases match patterns during provisioning:

- `modules/caddyhttp/matchers.go`: rationale captured in the `MatchPath` comment.
- `MatchPath.Provision` lowercases configured patterns via `strings.ToLower`.
- `MatchPath.MatchWithError` lowercases the request path for the normal matching path: `reqPath := strings.ToLower(r.URL.Path)`.

But when a match pattern contains a percent sign (`%`), `MatchPath.MatchWithError` switches to "escaped space" matching and builds the comparison string from `r.URL.EscapedPath()`:

- `reqPathForPattern := CleanPath(r.URL.EscapedPath(), mergeSlashes)`
- If it doesn't match, it `continue`s (skipping the remaining matching logic for that pattern).

Because `r.URL.EscapedPath()` is not lowercased, case differences in the request path can cause the escaped-space match to fail even though `MatchPath` is meant to be case-insensitive. For example, with a pattern of `/admin%2Fpanel`:

- Requesting `/admin%2Fpanel` matches and can be denied as intended.
- Requesting `/ADMIN%2Fpanel` does not match and falls through to other routes/handlers.

#### Suggested fix
- In the `%`-pattern matching path, ensure the effective string passed to `path.Match` is lowercased (same as the normal branch).
  - Simplest seems to lowercase the constructed string in `matchPatternWithEscapeSequence` right before `path.Match`.

Reproduced on:
- Stable release: `v2.10.2` -- this is the release referenced in the reproduction below.
- Dev build: `v2.11.0-beta.2`.
- Master tip: commit `58968b3fd38cacbf4b5e07cc8c8be27696dce60f`.

### PoC
Prereqs:
- bash, curl
- A pre-built Caddy binary available at `/opt/caddy-2.10.2/caddy` (edit `CADDY_BIN` in the script if needed)


<details>
<summary>Script (Click to expand)</summary>

```bash
#!/usr/bin/env bash
set -euo pipefail

CADDY_BIN="/opt/caddy-2.10.2/caddy"
HOST="127.0.0.1"
PORT="8080"

TMPDIR="$(mktemp -d)"
CADDYFILE="${TMPDIR}/Caddyfile"
LOG="${TMPDIR}/caddy.log"

cleanup() {
  if [ -n "${CADDY_PID:-}" ] && kill -0 "${CADDY_PID}" 2>/dev/null; then
    kill "${CADDY_PID}" 2>/dev/null || true
    wait "${CADDY_PID}" 2>/dev/null || true
  fi
  rm -rf "${TMPDIR}" 2>/dev/null || true
}
trap cleanup EXIT

if [ ! -x "${CADDY_BIN}" ]; then
  echo "error: missing caddy binary at ${CADDY_BIN}" >&2
  exit 2
fi

echo "== Caddy version =="
"${CADDY_BIN}" version

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

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

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

e