Public: anyone can read this; the crew can change it.
Origin discovery and migrate refuse a host that resolves to a private address, at every redirect hop #3049
Check · Review · Land · Live done — Landed and live in production.
Origin discovery and migrate now refuse a host that resolves to a private address, including at a redirect hop. `tsc` is clean and the origins and migrate tests pass (141, 0 failed). I did not run the full check.
shipped production is serving it deploy 2fa0c24
Origin discovery and migrate now refuse a host that resolves to a private address, including at a redirect hop. tsc is clean and the origins and migrate tests pass (141, 0 failed). I did not run the full check.
What changed
- Discovery (
src/origins/discover.ts): a host that resolves privately ends with a newprivate_hosterror. Each request goes throughoutboundFetch, so it is vetted and pinned to the judged address. A redirect to a private target also ends withprivate_host, e.g. 169.254.169.254.private_hosthas messages in the web, TUI and API. - Migrate (
src/actions/migrate.ts): after the literalisPrivateHostcheck, the host goes throughvetHost, so a name resolving to 10.0.0.5 getsprivate_host. Admins andFORGE_MIGRATE_PRIVATE_HOSTSstill bypass both checks, as before.migrateRepotakes an optionalresolveso tests can inject DNS. - Dev check: the repo had no dev check, so I added one. The loopback exception now applies only when
FORGE_DEV=1, andtest/helpers/preload.tssets that for the suite. Hosts named inFORGE_ORIGIN_ALLOWstill skip vetting. - Tests: a migrate from a name that resolves to 10.0.0.5 is refused. Discovery redirected to 169.254.169.254 is refused. Loopback is off without
FORGE_DEV. I edited two existing loopback assertions intest/origins-oauth.test.tsto passFORGE_DEV=1, since they now depend on dev mode. - Docs:
docs/ORIGINS.mdis updated.
Decisions for you
- Dev flag:
FORGE_DEV=1is a new env var, so a dev instance now has to set it to reach a local forge. Say if you'd rather key dev off something else. - Migrate DNS: the migrate check is a resolve-then-judge before git runs. git still does its own lookup, so a DNS rebind between the two stays possible. Pinning git to the vetted address was out of scope. An unresolvable name is left to git to fail on.
Try it: FORGE_DEV= bun run src/main.ts, then connect an origin at https://localhost. It is refused.
Approved. The change does what #2993 asks. discover vets the named host before any probe. Every same-origin hop goes through outboundFetch, which resolves the name, judges the address and dials that address. A hop to another origin is vetted only so the error can say which kind of failure it was, and it is never dialled. Migrate now runs vetHost after the literal isPrivateHost check, and admins and FORGE_MIGRATE_PRIVATE_HOSTS still skip both, as before. Loopback is reachable only with FORGE_DEV=1, and the repository had no dev check to reuse. Hosts named in FORGE_ORIGIN_ALLOW keep working and are dialled as typed. private_host has words in the web page, the JSON API (connect and origin-apps) and the TUI, so both doors read the same. The tests cover what the issue named: a name resolving to 10.0.0.5 is refused for migrate and for discovery, a 302 to 169.254.169.254 is refused, and loopback is off outside dev. Each would fail without the change. Neither the session-flow nor the body-cap paths are touched. The web change is one message string, so it raises nothing under DESIGN.md.
I could not open the preview or the screenshots from my sandbox. The network to the preview host and /data/looks are both denied. The change has no new UI beyond the error sentence.
Two things are left for follow-up issues. Neither blocks this pull, and both sit outside its scope:
- Migrate rebind (medium).
vetHostjudges the name, then git resolves it again on its own. A name with a zero TTL can answer public to the check and 10.x to git. Fix: pin git to the vetted address (http.curloptResolve/-c http.<url>.curloptResolve=host:port:ip). The builder said this was out of scope. - Calls after discovery (medium). Once discovery succeeds, the provider calls (
whoami, OAuth token exchange, API reads with the user's token) still use plainfetchagainstapiUrl. They are not pinned, so the same rebind reaches them. Fix: send provider HTTP for non-exempt hosts throughoutboundFetch.
One nit: FORGE_DEV is documented only in docs/ORIGINS.md. A line wherever the README lists environment settings would help whoever runs a dev instance and finds a local forge refused.
Origin discovery and migrate now refuse a host that resolves to a private address, including at a redirect hop.
tscis clean and the origins and migrate tests pass (141, 0 failed). I did not run the full check.What changed
src/origins/discover.ts): a host that resolves privately ends with a newprivate_hosterror. Each request goes throughoutboundFetch, so it is vetted and pinned to the judged address. A redirect to a private target also ends withprivate_host, e.g. 169.254.169.254.private_hosthas messages in the web, TUI and API.src/actions/migrate.ts): after the literalisPrivateHostcheck, the host goes throughvetHost, so a name resolving to 10.0.0.5 getsprivate_host. Admins andFORGE_MIGRATE_PRIVATE_HOSTSstill bypass both checks, as before.migrateRepotakes an optionalresolveso tests can inject DNS.FORGE_DEV=1, andtest/helpers/preload.tssets that for the suite. Hosts named inFORGE_ORIGIN_ALLOWstill skip vetting.FORGE_DEV. I edited two existing loopback assertions intest/origins-oauth.test.tsto passFORGE_DEV=1, since they now depend on dev mode.docs/ORIGINS.mdis updated.Decisions for you
FORGE_DEV=1is a new env var, so a dev instance now has to set it to reach a local forge. Say if you'd rather key dev off something else.Try it:
FORGE_DEV= bun run src/main.ts, then connect an origin athttps://localhost. It is refused.Closes #2993