ssrf and registry
Env, not a file.
GITHOST_INSTANCES=taf=https://git.taf.sh,codeberg=https://codeberg.org,gitea=https://gitea.com
GITHOST_TOKEN_TAF=
GITHOST_ALLOW_CUSTOM_BASE=true
GITHOST_BLOCK_PRIVATE_IPS=true
GITHOST_CUSTOM_CONCURRENCY=2
parse_instances(): comma-separated key=url, lowercase keys, origin must be https on port 443. Token from GITHOST_TOKEN_<KEY> with non-alnum turned into _. /healthz lists keys and whether a token is set. Tokens never appear in logs or OpenAPI.
resolve_target:
base_urlset andGITHOST_ALLOW_CUSTOM_BASE=false→ 403.- Else validate the origin. If its hostname equals a registered instance, return that Target (token and all). Else custom:
key=hostname,token=None, concurrency 2. - Else
hostas registry key or registered hostname. Else 404 listing keys. - Else 400: “No host selected.”
?base_url= beats ?host=. Prefix /f/{host}/... cannot take base_url.
validate_base_url before any fetch: https only, port 443, no userinfo, DNS on 443, every A/AAAA checked. Forbidden when block_private_ips: loopback, non-global, link-local, multicast, reserved, plus CGNAT 100.64.0.0/10 (Tailscale). HostClient does not follow redirects. Each hop re-enters the guard. Max 3 hops.
Caller-supplied hosts are always anonymous. The service does not accept a caller token to forward. Keep GITHOST_ALLOW_CUSTOM_BASE=false on a public Vercel. GITHOST_BLOCK_PRIVATE_IPS=false only if this process sits on a tailnet and must reach LAN forges.
Username is extracted from both path shapes so one client cannot fan out across hosts under the same rate-limit bucket.