GitHub Security Advisory: GHSA-x76f-jf84-rqj8
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: MatchHost becomes case-sensitive for large host lists (>100), enabling host-based route/auth bypass

Details:
### Summary
Caddy's HTTP `host` request matcher is documented as case-insensitive, but when configured with a large host list (>100 entries) it becomes case-sensitive due to an optimized matching path. An attacker can bypass host-based routing and any access controls attached to that route by changing the casing of the `Host` header.

### Details
In Caddy `v2.10.2`, the `MatchHost` matcher states it matches the Host value case-insensitively:

- `modules/caddyhttp/matchers.go`: `type MatchHost matches requests by the Host value (case-insensitive).`

However, in `MatchHost.MatchWithError`, when the host list is considered "large" (`len(m) > 100`):

- `MatchHost.large()` returns true for `len(m) > 100` (`modules/caddyhttp/matchers.go`, around the `large()` helper).
- The matcher takes a "fast path" using binary search over the sorted host list, and checks for an exact match using a case-sensitive string comparison (`m[pos] == reqHost`).
- After the fast path fails, the fallback loop short-circuits for large lists by breaking as soon as it reaches the first non-fuzzy entry. For configs comprised of exact hostnames only (no wildcards/placeholders), this prevents the `strings.EqualFold(reqHost, host)` check from ever running.

Net effect: with a host list length of 101 or more, changing only the casing of the incoming `Host` header can cause the `host` matcher to not match when it should.

#### Suggested fix
- Normalize exact hostnames to lower-case during `MatchHost.Provision` (at least for non-fuzzy entries).
- Normalize the incoming request host (`reqHost`) to lower-case before the large-list binary search + equality check, so the optimized path stays case-insensitive.

Reproduced on:
- Stable release: `v2.10.2` -- this is the release I reference in the repro 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
    @protected {
        host h001.test h002.test h003.test h004.test h005.test h006.test h007.test h008.test h009.test h010.test h011.test h012.test h013.test h014.test h015.test h016.test h017.test h018.test h019.test h020.test h021.test h022.test h023.test h024.t