Public: anyone can read this; nobody can change it.
The connect flow page goes on only to the authorize URL stored on the flow, so a crafted link cannot choose the client or the callback #69
Check · Review · Land · Live — No review yet.
**Security.** The connect flow page (`/settings/origins/flows/:id`) took its authorize URL from its own query string (`?to=`). It checked only two things: that the URL was on the instance's origin, and that it had a PKCE challenge. So a crafted link to a forge's flow page could choose another OAuth application's client id, with that application's callback, on the same instance. The page then offer…
0 files · +0 −0
waiting checks and the preview wait for a writer to approve this head
Check on trevato-apps/forge: running, as of
Checks and the preview wait for a writer to approve this head
A guest
This pull comes from a guest: its preview runs without the app's variables or a forge token until a person who can write here approves it.
This pull cannot land as it is, whatever its review says, because it changes guarded paths: (the diff) (it could not be read).
Security. The connect flow page (
/settings/origins/flows/:id) took its authorize URL from its own query string (?to=). It checked only two things: that the URL was on the instance's origin, and that it had a PKCE challenge. So a crafted link to a forge's flow page could choose another OAuth application's client id, with that application's callback, on the same instance. The page then offered it as "Continue to the instance". A person who trusted the forge's page could approve an application that the forge did not start. The flow row already seals the authorize URL (target_enc, for the short link), so the query string was never necessary.Now
flowTarget(src/origins/oauth.ts) opens the URL sealed on a pending flow, and the page links to that URL and to no other. The page checks the URL again first: the instance's own/login/oauth/authorize, the flow'sclient_idandredirect_uri, and acode_challenge. Starting a flow redirects to the bare flow page, withthen=migratewhen it started on the migrate page. Atothat an older link still has must be exactly the stored URL. Any othertogets "This connect cannot be continued from here". A pending flow past its ten minutes says "This connect link expired" and shows a fresh Connect for the same host. That button keepsthen=migrate.The device flow, the short link (
/go/:code), the poll and the callback are unchanged. Nothing new is stored, and there is no migration.Tests
bun 1.3.13, a scrubbed environment (no agent variables), Chromium 151:
test/origin-flow-page.test.ts: 5 pass, 0 fail. One of them is new here: a connect started from migrate keepsthen=migrateand noto, and its expired page starts again from migrate.test/oauth-connect-browser.test.ts(FORGE_CHROMIUMset, so it ran and did not skip): 1 pass, 0 fail. It clicks Authorize and Continue and checks that the consent screen is asked for with no CSP refusal.bun test --timeout 60000call: 432 pass, 0 fail across 33 files.bun scripts/gate.tsandtsc --noEmit: clean.the 33 files
origin-flow-page, oauth-connect-browser, origins, origins-oauth, origins-operator, origins-reconnect, origins-security-auth, origins-security-provider, origins-security-push, origins-sync, origins-sync-settled, web-origins, web-origin-link, mcp-origins, mcp-origin-apps, push-through, trusted-hosts, trusted-hosts-ui, steps-lists, no-polling, refresh-meta, mirror-outbound, mirror-out, carried, migrate, migrate-after, tui-migrate, apps-feedback-mirror, and the five cross files reserved-names, studio, shards, check-list, hollow (each
test/<name>.test.ts).🤖 Generated with Claude Code