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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

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

opened this

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 for http:// when the host is not localhost, 127.0.0.1 or [::1]. It compares the host as URL parses it, so LOCALHOST counts as loopback and localhost.evil.example does not. normalizeUrl is 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 prints forge login: plain http sends your token in clear; use https, or --insecure for a host you trust and exits 2 without making a request or writing a file.
  • --insecure: with this flag, login goes through and insecure: true is saved alongside the URL. It is only saved for a remote http address, the one case where it matters.
  • Later commands: loadConfig returns a new plain_http error before any request when the address (from FORGE_URL or the stored config) is remote http. The one exception is when the address matches the stored workbench that was logged in with --insecure. requireConfig and the terminal UI print forge: plain http sends your token in clear; use https, or --insecure for a host you trust and 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;
  • --insecure passes, is saved, and whoami works afterwards;
  • a remote http FORGE_URL is refused before any request, and a stored --insecure does not cover a different FORGE_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 x exits 2 with the refusal.
  • bun src/cli/main.ts login localhost:3000 still uses http.

🤖 Generated with Claude Code

Closes #7377

commented

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 for http:// when the host is not localhost, 127.0.0.1 or [::1]. It compares the host as URL parses it, so LOCALHOST counts as loopback and localhost.evil.example does not. normalizeUrl is 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 prints forge login: plain http sends your token in clear; use https, or --insecure for a host you trust and exits 2 without making a request or writing a file.
  • --insecure: with this flag, login goes through and insecure: true is saved alongside the URL. It is only saved for a remote http address, the one case where it matters.
  • Later commands: loadConfig returns a new plain_http error before any request when the address (from FORGE_URL or the stored config) is remote http. The one exception is when the address matches the stored workbench that was logged in with --insecure. requireConfig and the terminal UI print forge: plain http sends your token in clear; use https, or --insecure for a host you trust and 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;
  • --insecure passes, is saved, and whoami works afterwards;
  • a remote http FORGE_URL is refused before any request, and a stored --insecure does not cover a different FORGE_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 x exits 2 with the refusal.
  • bun src/cli/main.ts login localhost:3000 still uses http.

🤖 Generated with Claude Code

Approved

Approved. It does what #7377 asked and nothing more.

What I checked at 6f644b8:

  • plainHttpRemote compares the hostname the way URL parses it, so LOCALHOST, 127.1 and [0::1] count as loopback, and localhost.evil.example and 127.0.0.2 are refused. Refusing too much is the safe way to be wrong.
  • forge login http://example.com exits 2 before it asks for a token. It sends no request and writes no config. I checked this by hand with an empty FORGE_CONFIG_DIR.
  • FORGE_URL=http://10.1.2.3 is refused before any request, while loopback http still goes through. A stored --insecure covers 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 fetch drops authorization on 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 --noEmit is clean. test/cli*.test.ts passes: 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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs