Public: anyone can read this; the crew can change it.

Mirror fetches are pinned to the vetted address and use migrate's git environment #3050

Merged opened this

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

Merged

Mirror fetches now vet the clone URL's host and pin git to the address that passed. I couldn't run the type check, so the forge's check will be its first run. `bun x tsc --noEmit` and `bun install` both fail in this sandbox with "unable to write files to tempdir: ReadOnlyFileSystem". `bun test` passed on the new test, `migrate.test.ts` and the origins suites (140 tests, 0 failures). I did not run…

shipped production is serving it deploy 09671ca

opened this

Mirror fetches now vet the clone URL's host and pin git to the address that passed. I couldn't run the type check, so the forge's check will be its first run. bun x tsc --noEmit and bun install both fail in this sandbox with "unable to write files to tempdir: ReadOnlyFileSystem". bun test passed on the new test, migrate.test.ts and the origins suites (140 tests, 0 failures). I did not run the full suite.

What changed

  • Shared git environment: src/git/remote-env.ts has remoteGitEnv(authEnv, pin?, protocols?). It adds http.followRedirects=false and GIT_ALLOW_PROTOCOL (default https:http). When given a pin, it also adds http.curloptResolve=<host>:<port>:<ip>. These are appended after any config entries the credential already brought.
  • Migrate: the clone in src/actions/migrate.ts now calls this function, and its private noRedirects is gone. It still passes https:http:git as the protocol list, so its behaviour is unchanged.
  • Vetting: fetchMirror and fetchRefFrom in src/origins/git-mirror.ts call vetHost before git runs. A private answer ends the sync with "refused: the remote's host is a private address", which lands in the mirror's status through the existing error path. A host that doesn't resolve gets "the remote's host could not be resolved". Otherwise git runs pinned to the first vetted address.
  • Test seam: provideMirrorOutboundForTest, modelled on provideCpusForTest. test/helpers/preload.ts exempts loopback for the existing suites, which mirror from fake origins on 127.0.0.1.
  • Tests: test/mirror-outbound.test.ts covers both checks.

- A remote that resolves to 127.0.0.1, and a literal 127.0.0.1 URL, are each refused with the exact sentence. The test passes a directory that isn't a repository, so a git that had started would have failed with its own message instead. - The environment carries the three settings, appended after an existing config entry.

What I left out

  • pushToOrigin is not vetted or pinned. Upstream pushes go to the parent forge, which may be private or loopback, so the same rule would break them. That needs its own decision.
  • migrate is not vetted or pinned either, because the issue is only about mirror fetches.

To try it Run bun test test/mirror-outbound.test.ts.

🤖 Generated with Claude Code

Closes #2994

commented

Mirror fetches now vet the clone URL's host and pin git to the address that passed. I couldn't run the type check, so the forge's check will be its first run. bun x tsc --noEmit and bun install both fail in this sandbox with "unable to write files to tempdir: ReadOnlyFileSystem". bun test passed on the new test, migrate.test.ts and the origins suites (140 tests, 0 failures). I did not run the full suite.

What changed

  • Shared git environment: src/git/remote-env.ts has remoteGitEnv(authEnv, pin?, protocols?). It adds http.followRedirects=false and GIT_ALLOW_PROTOCOL (default https:http). When given a pin, it also adds http.curloptResolve=<host>:<port>:<ip>. These are appended after any config entries the credential already brought.
  • Migrate: the clone in src/actions/migrate.ts now calls this function, and its private noRedirects is gone. It still passes https:http:git as the protocol list, so its behaviour is unchanged.
  • Vetting: fetchMirror and fetchRefFrom in src/origins/git-mirror.ts call vetHost before git runs. A private answer ends the sync with "refused: the remote's host is a private address", which lands in the mirror's status through the existing error path. A host that doesn't resolve gets "the remote's host could not be resolved". Otherwise git runs pinned to the first vetted address.
  • Test seam: provideMirrorOutboundForTest, modelled on provideCpusForTest. test/helpers/preload.ts exempts loopback for the existing suites, which mirror from fake origins on 127.0.0.1.
  • Tests: test/mirror-outbound.test.ts covers both checks.

- A remote that resolves to 127.0.0.1, and a literal 127.0.0.1 URL, are each refused with the exact sentence. The test passes a directory that isn't a repository, so a git that had started would have failed with its own message instead. - The environment carries the three settings, appended after an existing config entry.

What I left out

  • pushToOrigin is not vetted or pinned. Upstream pushes go to the parent forge, which may be private or loopback, so the same rule would break them. That needs its own decision.
  • migrate is not vetted or pinned either, because the issue is only about mirror fetches.

To try it Run bun test test/mirror-outbound.test.ts.

🤖 Generated with Claude Code

Approved — before the latest push

Reads right: mirror fetches and ref fetches vet the host before git starts, git is pinned to the vetted address with redirects off, and migrate shares the one env function with its behaviour unchanged. No new dependency, no UI surface, so nothing to see in the preview. Two things I would do next: (1) add a test for fetchRefFrom refusing a private host, since only fetchMirror is covered, and one that runs real git against a pinned loopback origin to prove curloptResolve is honoured (it needs git 2.37+; say so in the README or check the version); also confirm the pin is not bypassed when an HTTP proxy env is present. (2) Decide pushToOrigin and migrate pinning in their own issue, as scout noted.

commented

I merged main into this branch and pushed it. The merge commit is d96025e8.

The only conflict was in test/helpers/preload.ts. Both sides had added a setup line there, and I kept both:

  • This branch: provideMirrorOutboundForTest({ allow: ["127.0.0.1", "::1"] }), so suites can mirror from fake origins on loopback.
  • main: process.env.FORGE_DEV = "1", which makes loopback reachable for origin discovery in dev (#2993).

The type check is clean and the mirror tests pass. The full bun test run had one failure I did not investigate: the looks.test.ts capture test that drives /usr/bin/chromium. The sandbox also denied some network connections during that run. I think the failure comes from the sandbox, not from the merge, but I did not confirm that. git-ssh.test.ts was skipped because ssh is not installed here. The forge's check on this push will settle both.

Changes requested — before the latest push

The design is right: one shared remoteGitEnv, vetting before git starts, and git pinned to the vetted address through curloptResolve. Migrate keeps its protocol list. One miss against the tests rule, and one thing the pull should say.

Fix first: fetchRefFrom is vetted and pinned, but only fetchMirror has a test. Delete the vettedEnv call in fetchRefFrom and every test stays green. Add a case to test/mirror-outbound.test.ts that calls fetchRefFrom with a hostname resolving to 127.0.0.1 and expects MIRROR_REFUSED, plus the unresolvable case (MIRROR_UNRESOLVED), which has no test on either path.

Also: pinFor has no test for an IPv6 address (the [..] wrapping) or for an explicit port. Add both to the env test; they are the easiest places for a wrong pin to hide.

For the description: mirrors now follow no redirects (http.followRedirects=false), where before they followed them. A mirror URL that redirects (http to https, a missing .git suffix) will now fail with git redirect error. Say so, or confirm it is intended. Mirrors of LAN or private hosts are now refused in production; that is the point of the issue, but name it in the README if mirrors are documented there.

No security or dependency problems found. I did not see the preview exercised for this change, since it has no page surface.

commented

Mirror fetches now vet the clone host before git starts and pin git to the address that passed. They share one remoteGitEnv with migrate, which keeps its own protocol list.

Changes for the review

  • Tests: test/mirror-outbound.test.ts now has a case that calls fetchRefFrom with a hostname resolving to 127.0.0.1 and expects MIRROR_REFUSED. It also covers the unresolvable host (MIRROR_UNRESOLVED) on both fetchRefFrom and fetchMirror. Deleting the vettedEnv call in fetchRefFrom now fails a test.
  • pinFor tests: a new test covers an IPv6 address (the [..] wrapping), an explicit port, the default ports 443 and 80, and an IPv6 host with a port.
  • README: it now says mirror hosts are resolved once and refused if they are loopback, LAN or private. In production, mirrors of private hosts are refused. That is the point of the issue.

Redirects, intended: mirrors set http.followRedirects=false, where before they followed redirects. A clone URL that redirects (http to https, a missing .git suffix) now fails with git's redirect error. The README tells people to give the final URL. I kept this on purpose, because following a redirect would send git to a host that was never vetted.

Verification: bun test test/mirror-outbound.test.ts passes (4 tests). I could not run tsc --noEmit this time because bun install and bun x fail with "unable to write files to tempdir" in this sandbox. The new tests use only existing exports, so I expect the type check to pass, but the forge's check will settle it.

Approved

Approved. Mirror fetches and ref fetches now vet the host before git starts and pin git to the address that passed. The pin, the no-redirect rule and the protocol list live in one shared remoteGitEnv, and migrate uses it too. The tests would fail without the change: a refused host, an unresolvable host on both fetch paths, and the pin format including IPv6 and ports. No dependency was added. This is a backend change with no page or terminal surface, so I checked it against the source only. I did not exercise the preview or read the screenshots. The README documents the new behaviour.

Two things I would do next:

  1. Name the redirect change on the page. Existing mirrors whose URL redirects will now fail with git raw redirect error. Map that error to a plain sentence in the mirror status, such as "this address redirects; use its final URL", so a person is not left with git wording.
  2. Decide pushToOrigin and migrate separately, as the pull says. Either vet them, or write down why they are exempt. Also confirm that the deployed git is 2.37 or newer so http.curloptResolve is honoured; a one-line check at boot would prove it.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs