# Tinyauth: forward-auth per-app ACL is matched case-sensitively against the (case-insensitive) hostname, letting an authenticated user reach apps they are not on the allowlist for

Tinyauth's per-app ACL lookup compares the forwarded host case-sensitively; a mixed-case Host misses the ACL and fails open, letting any logged-in user reach apps they're not allowlisted for.

- Payout: Research
- Severity: high
- Program: Tinyauth
- Found by: 5ud0er
- Disclosed: 2026-09-22
- CVE: CVE-2026-77560
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-328g-jx67-v94g
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-328g-jx67-v94g
- Tags: area:cloud domain:infra domain:web impact:internal-access impact:privesc kind:research platform:ghsa sev:high tech:go technique:source-review vuln:access-control vuln:auth-bypass vuln:misconfiguration

**Target:** forward-auth middleware (Traefik/Caddy/nginx/Envoy integrations)
**Root cause:** lookupStaticACLs/GetLabels compare the forwarded host to configured domains with case-sensitive `==`, while hostnames are case-insensitive; on a miss GetLabels returns an empty config.App with nil error, and an empty App's empty users.allow passes CheckFilter, authorizing everyone.
**Steps:**
1. Deploy tinyauth v5.0.7 with an app `immich` whose ACL sets `users.allow: admin`.
2. Authenticate as low-privileged user `bob` (not in allowlist).
3. Send `GET /api/auth/traefik` with `x-forwarded-host: immich.example.com` -> 403 (control).
4. Repeat with `x-forwarded-host: IMMICH.example.com` -> ACL lookup misses, Docker fallback returns empty App, response is `200 Authenticated` with `Remote-User: bob`.
5. In a live deployment: `curl -H 'Host: IMMICH.example.com' https://<proxy>/` with bob's session cookie; proxy routes to immich and forwards mixed-case host.
**How found:** Code review of the ACL lookup path: noticed `config.Config.Domain == domain` and `strings.SplitN(domain, ".", 2)[0] == app` are case-sensitive while DNS/HTTP/SNI are case-insensitive, then traced the miss path into GetLabels returning an empty App with nil error and CheckFilter("", ...) returning true — a fail-open.
**Exploitation:** An authenticated low-privileged user requests a protected app through the reverse proxy using a mixed-case (or trailing-dot / port-variant) hostname. The proxy routes case-insensitively to the intended backend but forwards the mixed-case host in X-Forwarded-Host/X-Original-URL/Host; tinyauth's case-sensitive ACL lookup misses, falls back to an empty App, and returns 200 Authenticated, emitting Remote-User/Remote-Email headers so downstream apps trust the attacker as authorized.
**Escalation:** Bypass applies to all four proxy integrations and to users/groups/ip/oauth.whitelist ACLs, granting full read/write access to any app on the same tinyauth instance; trailing-dot and port variants give equivalent encodings.
**Insight:** When a security decision keys off a hostname, check whether the comparison is byte-exact while every layer around it (DNS, proxy routing, SNI) is case-insensitive; also trace the 'no match' branch — a lookup miss that returns a zero-value config object often silently means allow-all.

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

**curl request with mixed-case Host header**

```
curl -H 'Host: IMMICH.example.com' https://<proxy>/
```

