Public: anyone can read this; the crew can change it.
`forge login` refuses a plain http:// workbench that is not loopback, so a token never crosses the network in clear #7397
Check · Review · Land · Live done — Landed and live in production.
The `forge` CLI now refuses to send a token over plain `http://` to another machine. A new test file covers this (9 tests, all passing), and the type check is clean. The 20 test files `scripts/fast.ts` named for this diff also pass (222 tests). The branch is pushed.
shipped production is serving it deploy aafe9dc
The forge CLI now refuses to send a token over plain http:// to another machine. A new test file covers this (9 tests, all passing), and the type check is clean. The 20 test files scripts/fast.ts named for this diff also pass (222 tests). The branch is pushed.
- The check (
src/cli/config.ts):plainHttpRemote(url)is true forhttp://when the host is notlocalhost,127.0.0.1or[::1]. It compares the host asURLparses it, soLOCALHOSTcounts as loopback andlocalhost.evil.exampledoes not.normalizeUrlis unchanged: a bare host still becomes https and loopback still becomes http. forge login(src/cli/main.ts): it refuses a remote http address before asking for the token. It printsforge login: plain http sends your token in clear; use https, or --insecure for a host you trustand exits 2 without making a request or writing a file.--insecure: with this flag, login goes through andinsecure: trueis saved alongside the URL. It is only saved for a remote http address, the one case where it matters.- Later commands:
loadConfigreturns a newplain_httperror before any request when the address (fromFORGE_URLor the stored config) is remote http. The one exception is when the address matches the stored workbench that was logged in with--insecure.requireConfigand the terminal UI printforge: plain http sends your token in clear; use https, or --insecure for a host you trustand exit 2. So someone already logged in to a remote http workbench sees this line on their next command, not a silent failure. - The usage line shows
[--insecure], and the README's CLI section explains the rule.
test/cli-plain-http.test.ts runs main() itself with fetch stubbed and every request counted. It proves:
- a remote http login is refused, sends nothing and writes nothing;
- loopback http and https logins pass;
--insecurepasses, is saved, andwhoamiworks afterwards;- a remote http
FORGE_URLis refused before any request, and a stored--insecuredoes not cover a differentFORGE_URL; - an existing remote http config gets the refusal on
sync.
Not done: there is no environment variable that does what --insecure does. Scripts that need a remote http workbench run forge login --insecure once.
To try it:
bun src/cli/main.ts login http://example.com --token xexits 2 with the refusal.bun src/cli/main.ts login localhost:3000still uses http.
🤖 Generated with Claude Code
Approved. It does what #7377 asked and nothing more.
What I checked at 6f644b8:
plainHttpRemotecompares the hostname the wayURLparses it, soLOCALHOST,127.1and[0::1]count as loopback, andlocalhost.evil.exampleand127.0.0.2are refused. Refusing too much is the safe way to be wrong.forge login http://example.comexits 2 before it asks for a token. It sends no request and writes no config. I checked this by hand with an emptyFORGE_CONFIG_DIR.FORGE_URL=http://10.1.2.3is refused before any request, while loopback http still goes through. A stored--insecurecovers only its own normalized URL.- An existing remote-http config gets the refusal's exact words on its next command, which covers the risk the issue named.
- Bun's
fetchdropsauthorizationon a cross-origin redirect (I tested this locally), so an https workbench that redirects to http does not leak the token through the API client. bun x tsc --noEmitis clean.test/cli*.test.tspasses: 119 tests across 12 files, including the 9 new ones. Each test would fail without the change. The preview answers/with 200.
Outside this issue's scope, filed as #7400: forge sync still hands the token to git through http.extraHeader for whatever clone or origin URL it has, and nothing checks that URL's scheme or host.
The
forgeCLI now refuses to send a token over plainhttp://to another machine. A new test file covers this (9 tests, all passing), and the type check is clean. The 20 test filesscripts/fast.tsnamed for this diff also pass (222 tests). The branch is pushed.src/cli/config.ts):plainHttpRemote(url)is true forhttp://when the host is notlocalhost,127.0.0.1or[::1]. It compares the host asURLparses it, soLOCALHOSTcounts as loopback andlocalhost.evil.exampledoes not.normalizeUrlis unchanged: a bare host still becomes https and loopback still becomes http.forge login(src/cli/main.ts): it refuses a remote http address before asking for the token. It printsforge login: plain http sends your token in clear; use https, or --insecure for a host you trustand exits 2 without making a request or writing a file.--insecure: with this flag, login goes through andinsecure: trueis saved alongside the URL. It is only saved for a remote http address, the one case where it matters.loadConfigreturns a newplain_httperror before any request when the address (fromFORGE_URLor the stored config) is remote http. The one exception is when the address matches the stored workbench that was logged in with--insecure.requireConfigand the terminal UI printforge: plain http sends your token in clear; use https, or --insecure for a host you trustand exit 2. So someone already logged in to a remote http workbench sees this line on their next command, not a silent failure.[--insecure], and the README's CLI section explains the rule.test/cli-plain-http.test.tsrunsmain()itself withfetchstubbed and every request counted. It proves:--insecurepasses, is saved, andwhoamiworks afterwards;FORGE_URLis refused before any request, and a stored--insecuredoes not cover a differentFORGE_URL;sync.Not done: there is no environment variable that does what
--insecuredoes. Scripts that need a remote http workbench runforge login --insecureonce.To try it:
bun src/cli/main.ts login http://example.com --token xexits 2 with the refusal.bun src/cli/main.ts login localhost:3000still uses http.🤖 Generated with Claude Code
Closes #7377