What changed
- lanGuard refuses (403) a loopback request that carries a browser's
marks (Origin, Sec-Fetch-Site/Mode, a Mozilla/ user agent) when its Host
isn't this computer's: an IP, localhost, *.localhost, a container
runtime's name for the host (host.docker.internal and kin),
MAGPIE_PUBLIC_URL's or MAGPIE_ADDR's host, or the machine's own name.
An enabled gateway key still lets it in.
- Why: a page whose DNS answers 127.0.0.1 after it loads is same-origin
with the gateway. Its GET sends no Origin and its POST one equal to the
Host, so corsGuard's foreignPage let it by, and on loopback no key is
asked. The nightly security review reproduced it on origin/main: Host
and Origin rebind.attacker.example:3425 from 127.0.0.1 got 200 on
GET /v1/models, POST /v1/chat/completions (spends subscriptions) and
GET /v1/magpie/quotas (account names, balances). The subsystem doc
listed it as a known gap.
- Agents send none of a browser's marks and pass under any name; browsers
and webviews calling localhost or an IP are unchanged.
- Tests: TestCORSKeyThroughTheServer and the Omarchy test now set a real
Host (httptest's default is example.com). TestForeignPagesRefused's
"own origin behind a port-443 name" row from loopback with no forwarding
header now sends a key: without one it is the rebound page.
- docs/subsystems/gateway-routing.md describes the check in place of the gap.
Ran
- TestReboundPageRefused: fails on origin/main ("a rebound page's GET:
200"), passes with the change.
- go test -count=1 ./internal/gateway/ ./internal/gui/ ./internal/access/
(macOS, go1.27): ok. gofmt -l clean; GOOS=linux and GOOS=windows go vet
./internal/gateway/ clean.
Not tried: a real browser against a real rebinding DNS name.