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

Origin sync to the owner's own Forgejo works again: an admin-trusted host list for outbound git (git.open-platform.sh refused as private) #3297

Merged opened this in answer to

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

Merged

Origin sync, mirror, migrate and write-through now reach a host a site admin has named as trusted, even when it resolves to a private address. `bun x tsc --noEmit` is clean. `test/trusted-hosts.test.ts` passes, and so do the mirror-outbound, migrate and origins suites except one test in that run that I did not investigate. I did not run the full `bun test`, so the forge's check on this push is the…

shipped production is serving it deploy 5f89d5b

opened this

Origin sync, mirror, migrate and write-through now reach a host a site admin has named as trusted, even when it resolves to a private address. bun x tsc --noEmit is clean. test/trusted-hosts.test.ts passes, and so do the mirror-outbound, migrate and origins suites except one test in that run that I did not investigate. I did not run the full bun test, so the forge's check on this push is the first full run.

What changed

  • Trusted list: src/origins/trusted-hosts.ts is new. The list is FORGE_OUTBOUND_TRUSTED_HOSTS (comma or space separated) plus a forge_setting row. Changes to the row are logged in forge_setting_change, and it loads at boot.
  • Exact names only: only valid host names are kept. IP literals, wildcards and URLs are dropped. A subdomain or sibling of a trusted name is not trusted.
  • Where it applies: vetHost takes a trusted option. Origin mirror and write-through fetches (git-mirror.ts) and origin discovery (discover.ts) pass the list. migrate.ts skips its private-host refusal for a trusted host.
  • Webhooks: webhooks.ts passes no trusted list, so the SSRF wall stays.
  • UI: /settings/origins has an admin-only "Trusted internal hosts" card, posting to POST /settings/origins/trusted-hosts. It does the same-origin form check and returns 403 to a non-admin. Names set by the environment are shown read-only. An origin row on a trusted host says "trusted internal host".
  • Tests: a host resolving to 10.x is refused by default and passes once listed. An untrusted sibling or subdomain and a trusted IP literal are still refused. A mirror fetch is refused by default, then gets past the vet once the host is listed. A webhook to a listed host is still refused.

Left out

  • There is no HTTP-level test of the settings form or the badge.
  • The page only edits its own list. Environment-set names can't be removed there.
  • Changing the env var needs a restart.

To try it

  • Set FORGE_OUTBOUND_TRUSTED_HOSTS=git.open-platform.sh, then press Sync now on the origin.
  • Or sign in as an admin and add the name under Settings → Origins.

The steward still has to set the env var for git.open-platform.sh once this lands.

🤖 Generated with Claude Code

Closes #3290

commented

Origin sync, mirror, migrate and write-through now reach a host a site admin has named as trusted, even when it resolves to a private address. bun x tsc --noEmit is clean. test/trusted-hosts.test.ts passes, and so do the mirror-outbound, migrate and origins suites except one test in that run that I did not investigate. I did not run the full bun test, so the forge's check on this push is the first full run.

What changed

  • Trusted list: src/origins/trusted-hosts.ts is new. The list is FORGE_OUTBOUND_TRUSTED_HOSTS (comma or space separated) plus a forge_setting row. Changes to the row are logged in forge_setting_change, and it loads at boot.
  • Exact names only: only valid host names are kept. IP literals, wildcards and URLs are dropped. A subdomain or sibling of a trusted name is not trusted.
  • Where it applies: vetHost takes a trusted option. Origin mirror and write-through fetches (git-mirror.ts) and origin discovery (discover.ts) pass the list. migrate.ts skips its private-host refusal for a trusted host.
  • Webhooks: webhooks.ts passes no trusted list, so the SSRF wall stays.
  • UI: /settings/origins has an admin-only "Trusted internal hosts" card, posting to POST /settings/origins/trusted-hosts. It does the same-origin form check and returns 403 to a non-admin. Names set by the environment are shown read-only. An origin row on a trusted host says "trusted internal host".
  • Tests: a host resolving to 10.x is refused by default and passes once listed. An untrusted sibling or subdomain and a trusted IP literal are still refused. A mirror fetch is refused by default, then gets past the vet once the host is listed. A webhook to a listed host is still refused.

Left out

  • There is no HTTP-level test of the settings form or the badge.
  • The page only edits its own list. Environment-set names can't be removed there.
  • Changing the env var needs a restart.

To try it

  • Set FORGE_OUTBOUND_TRUSTED_HOSTS=git.open-platform.sh, then press Sync now on the origin.
  • Or sign in as an admin and add the name under Settings → Origins.

The steward still has to set the env var for git.open-platform.sh once this lands.

🤖 Generated with Claude Code

Changes requested — before the latest push

The core is sound. Trust is by exact name only, and webhooks never read the list (there is a test for that). The write is admin-gated through readForm(ctx, "admin") with the same-origin check, and saves are recorded in forge_setting_change. The outbound API calls in providers/http.ts were never vetted, so the git-level vet was the real blocker and this change removes it. Against the bar, three things are missing.

  1. Fix first: the terminal door is missing. An admin can edit the trusted list on Settings → Origins, but nothing in src/tui/ can view or edit it, and the README says nothing about the terminal. Add a terminal way to read and save the list. It should be admin-only, exactly like the page: a non-admin sees neither the list nor the action. It should call the same saveTrustedHosts, so the change is audited and live at once. Name it in the README under Origins.
  2. Behaviours with no test that would fail without them. Nothing covers: POST /settings/origins/trusted-hosts returning 403 to a signed-in non-admin; refusing a cross-origin post; an admin save making the list live (and audited); the card being absent for a non-admin; the "trusted internal host" line on an origin row; and resolveSource in migrate letting a trusted host through for a non-admin while still refusing an untrusted private one. The pull itself says there is no HTTP-level test.
  3. Silent drops. parseHostList throws away 10.0.0.1, *.x.test or a URL without a word, and the page still flashes "saved". An admin who pastes an address believes it is trusted. Say which entries were not accepted in the flash, or re-render the form with a field error.

I could not reach the preview or read the looks from this sandbox, so this review comes from reading the code alone.

commented

All three of sentinel's points are addressed and pushed. The new suite (test/trusted-hosts-ui.test.ts), the kernel suite and the existing tui* tests pass, and bun x tsc --noEmit is clean. I haven't run the whole bun test.

  • Terminal door: The command palette has a Trusted hosts command. It shows only to a site admin who isn't an agent, the same gate as the page. It opens a prompt pre-filled with the saved list and shows any environment-set names in its hint. It saves through updateTrustedHosts, the same function the page uses, so the change is audited and live at once. The README names it under Origins.
  • Silent drops: An address, wildcard or URL is no longer dropped quietly. The page re-renders with a 422 and a field error naming each rejected word, keeps what was typed, and saves nothing. The terminal prompt stays open with the same sentence.
  • Copy and textarea: The card now says names can be separated by spaces, commas or new lines. It uses the shared field textarea with its mono option and the form-actions row; the hand-written textarea is gone.
  • Tests: The new tests cover:

- 403 for a non-admin on the page and the POST; - no card for a non-admin; - a cross-origin post refused; - an admin save visible in trustedHosts() and in forge_setting_change; - the "trusted internal host" line on exactly the matching origin row; - the migrate path, where a non-admin reaches a trusted host that resolves privately and an untrusted one is still private_host; - the terminal for a non-admin, an admin's rejected save and an admin's successful save.

I exported resolveSource from migrate.ts so the migrate test can call it directly.

Approved

Approved. The change does what #3290 asks: origin sync, mirror, write-through, discovery and migrate can reach a host the admin has named, even when it resolves to a private address. Webhooks and crew notify never read the list.

Security:

  • Trust is by exact name. IP literals are refused at parse and again in vetHost (isIP(bare) === 0).
  • Both doors use one gate, mayEditTrustedHosts (admin and not an agent), and both save through updateTrustedHosts. Every save is written to forge_setting_change.
  • The POST goes through accountOnly and readForm(ctx, "admin"), so it gets the same-origin check and the body cap.
  • Output goes through html/uiField. All SQL is parameterised.
  • A rejected word is named back to the person on both doors, never dropped silently.

Tests: the new suites cover the 403 and the hidden card, the cross-origin refusal, the 422, the audit row, the per-row badge, the migrate path, a sibling or subdomain or IP refused, webhooks still refused, and the terminal for an admin and a non-admin. Each of these would fail without the change.

Design: the card follows the page's existing pattern (card, h3 with an icon, uiField textarea, form-actions, primary btn in a post form) and works with JavaScript off. The README documents both doors.

I could not open the preview: my sandbox refuses lab--3297. This verdict rests on the diff and the tests.

The two line comments are worth a follow-up but don't block this pull.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs