Host-based fetch is an unauthenticated LAN reverse proxy #12

Closed
opened 2026-08-31 18:56:17 -05:00 by Ghost · 1 comment

Severity: Medium

When upstream is empty (the generated default), SteamCache2 fetches http(s)://{request Host}/{path} for any GET whose User-Agent matches the Steam patterns (Steam, SteamClient, Valve/Steam HTTP Client 1.0). Those strings are trivial to send. Combined with the default listen address :80 (all interfaces), any host that can reach the cache can:

  • Use it as an unauthenticated reverse proxy to literal IPs on the LAN, localhost, or link-local metadata addresses, and to arbitrary DNS names.
  • Poison the shared cache: keys are steam/ + SHA256(path) with no Host. A request for the same depot path with a hostile Host fills the entry that real Steam clients will later hit. Steam will usually reject bad chunk hashes (failed downloads), not execute them — still a practical outage for everyone on the LAN cache.
  • Follow HTTP redirects from that origin, so a 302 can move the fetch onto a different address than the original Host.

This is the intended LAN-cache shape (DNS intercept, Host = CDN name), but there was no check that Host is actually a Steam CDN.

Fix

  • If upstream is set, keep fetching only that origin (unchanged).
  • If upstream is empty, allow Host only when it is a Steam CDN name (steamcontent.com, steampowered.com, steamstatic.com, including subdomains). Reject literal IPs.
  • Do not follow upstream redirects.
  • Leave path-only cache keys so *.steamcontent.com still share one object.

Do not bind :80 to the internet; keep this on the LAN. Set upstream to a known Steam origin if you do not want Host-based fetch at all.

A PR with the allowlist + redirect change is open.

## Severity: Medium When `upstream` is empty (the generated default), SteamCache2 fetches `http(s)://{request Host}/{path}` for any GET whose User-Agent matches the Steam patterns (`Steam`, `SteamClient`, `Valve/Steam HTTP Client 1.0`). Those strings are trivial to send. Combined with the default listen address `:80` (all interfaces), any host that can reach the cache can: - Use it as an unauthenticated reverse proxy to **literal IPs** on the LAN, localhost, or link-local metadata addresses, and to **arbitrary DNS names**. - **Poison the shared cache**: keys are `steam/` + SHA256(path) with **no Host**. A request for the same depot path with a hostile Host fills the entry that real Steam clients will later hit. Steam will usually reject bad chunk hashes (failed downloads), not execute them — still a practical outage for everyone on the LAN cache. - Follow **HTTP redirects** from that origin, so a 302 can move the fetch onto a different address than the original Host. This is the intended LAN-cache shape (DNS intercept, Host = CDN name), but there was no check that Host is actually a Steam CDN. ### Fix - If `upstream` is set, keep fetching only that origin (unchanged). - If `upstream` is empty, allow Host only when it is a Steam CDN name (`steamcontent.com`, `steampowered.com`, `steamstatic.com`, including subdomains). Reject literal IPs. - Do not follow upstream redirects. - Leave path-only cache keys so `*.steamcontent.com` still share one object. Do not bind `:80` to the internet; keep this on the LAN. Set `upstream` to a known Steam origin if you do not want Host-based fetch at all. A PR with the allowlist + redirect change is open.

Triaged as medium. Merged #13 (CDN Host allowlist + no redirect follow). Leave :80 on the LAN.

Triaged as medium. Merged #13 (CDN Host allowlist + no redirect follow). Leave :80 on the LAN.
Ghost closed this issue 2026-08-31 18:58:55 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: s1d3sw1ped/steamcache2#12