Skip to content

Wire safe-fetch into forge fetches (SHIP_SAFE_FETCH, shadow by default) - #44

Merged
im-tyler merged 2 commits into
mainfrom
claude/w3-safe-fetch
Oct 4, 2026
Merged

im-tyler merged 2 commits into
mainfrom
claude/w3-safe-fetch

Conversation

@im-tyler

@im-tyler im-tyler commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Changes

  • New src/forge-egress.ts: production pinned fetch (undici Agent whose connect.lookup returns the validated address; hostname kept for SNI/Host), operator allow-list, decideEgress, and forgeFetch.
  • src/git.ts and src/forge-state.ts: the DEFAULT fetch (?? fetch / = fetch) becomes forgeFetch. Injected fetchImpl paths are untouched. forgeFetch calls globalThis.fetch at call time, so tests that replace it still work.
  • Flag SHIP_SAFE_FETCH: unset/shadow (default) leaves the request untouched and logs once per host+reason what enforcement would refuse (check runs beside the request, never in front of it); on enforces (refuse, validate every redirect hop, connect to the validated address only, credentials stripped off-origin); off disables everything.
  • SHIP_SAFE_FETCH_ALLOW=host1,host2:3000,[::1]:8080: listed hosts are trusted by name (private addresses, http, any/listed port, single-label names). Metadata and link-local answers stay refused even for listed hosts.

Checks

  • pnpm run lint clean; pnpm test 1868/1868 on dist; web pnpm test 156/156, pnpm run build ok.
  • 17 new tests in src/forge-egress.test.ts, including a REAL loopback http server: refused under the default policy (server sees zero requests), reachable with an explicit allow, connection pinned against a rebinding resolver (resolver called once, Host header is the name), pinnedFetch to an unresolvable name, redirect hop refusal, retrieveUntrusted with the production fetch, and the git.ts call path (findOpenPullRequest) refused when enforcing.
  • Default-off equivalence: tests assert shadow passes identical arguments to the passthrough, off makes no resolver call, and the unflagged git.ts path hits the replaced global fetch with the same URL.
  • Negative controls (broke code, saw failures, restored): removing the deny throw fails 3 tests; making connect.lookup do real DNS fails 2 pin tests.
  • scripts/*.test.mjs has 3 failures (matrix, pj-b-deploy-recovery grader, committed report totals). They fail identically on origin/main without this change.

Not verified / still open

  • Not wired: the web setup route, ladder-steps, worker and deploy entry points get no submit-time refusal; they are covered only where their forge calls go through git.ts/forge-state.ts default fetch. git subprocess clone/fetch of the user URL is not guarded.
  • No verified outcome against a real Forgejo or a private network; the allow-list syntax is unproven in a deployment.
  • Shadow mode's extra DNS lookup per forge call is unmeasured.
  • Non-string Request inputs are rejected when enforcing (forge code only passes strings).

🤖 Generated with Claude Code

https://claude.ai/code/session_01VqsBNqvaWezf1DwQrAnVgX


Generated by Claude Code

claude added 2 commits October 4, 2026 02:44
…default)

Adds src/forge-egress.ts: production pinned fetch (undici Agent whose
connect.lookup returns the validated address), operator allow-list
(SHIP_SAFE_FETCH_ALLOW) for self-hosted forges, and a forge fetch used as
the default in git.ts and forge-state.ts. Default is shadow: the request is
untouched and what enforcement would refuse is logged once per host and
reason. SHIP_SAFE_FETCH=on enforces and validates every redirect hop;
=off disables the check entirely.

Tested on a real loopback socket (refused by default, reachable when
explicitly allowed, connection pinned against a rebinding resolver).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VqsBNqvaWezf1DwQrAnVgX
Keeps the forge-egress default fetch and the inline comments option from the
finding continuity change in forge-state.ts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VqsBNqvaWezf1DwQrAnVgX
@im-tyler
im-tyler merged commit fd8a8d3 into main Oct 4, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants