Repository navigation
Make iframe previews local-first, with an external-origin warning and allowlist - #1474
Conversation
… allowlist The iframe proxy fronted any http:// upstream it was handed, stripping a remote site's framing headers and CSP and serving it from the shared loopback hostname, with nothing telling the user that a remote page had lost the protections its author relied on. And a frame load redirected off the grant's upstream origin was forwarded verbatim, so the webview's loopback-only frame-src refused it as a blank pane. The grant is the only chokepoint that matters: both webview CSPs admit only loopback frames, so a remote page reaches a pane only through a minted grant. So the proxy now classifies an upstream by the addresses the grant is pinned to (every one loopback, never the hostname) and refuses a non-local one as `external` unless the request says the user allowed its exact origin; a leased external grant is not reused once the allowance is withdrawn. Each host forwards only an exact `allowExternal: true`. The panel shows an "External website preview" warning with Open in browser, Allow this origin, and Cancel. Allowed origins persist per host install in localStorage, listed and removable in Settings -> General; allowing or removing one re-asks the host, so a removal closes an open preview. A frame load redirected to another origin is answered with a Dormouse page that posts `navigate` to the app, which takes it as a URL-bar entry through the same gate, replacing the redirecting history entry; a frame nested in a proxied page follows the redirect itself, outside the webview CSP. Cookies stay stripped and https:// stays refused; in-place following of off-proxy links and forms, and cookie support, stay under ## Future. Closes #572. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exports ExternalPreviewPrompt so its layout and actions have a story: each action reaches its own handler, a host with no system browser drops Open in browser, and a long origin wraps in a narrow pane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ned asked for the gate to be an audited rule. security-local.md -> Browser panes gains a Guarantee row and FAIL IF S294 (claimed by the local domain's local-terminal stream, which already holds that heading); dor-browser.md's two bolded rules become the mechanism pointing at it. S294 skips S292 and S293, which the unmerged issue-1166-revoke-access-log takes; they sit in retired here only so the ledger lint passes on this branch alone, and come out of retired when the two branches meet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deploying mouseterm with
|
| Latest commit: |
59a718d
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://b294fcab.mouseterm.pages.dev |
| Branch Preview URL: | https://issue-572-iframe-local-first.mouseterm.pages.dev |
dormouse-bot
left a comment
There was a problem hiding this comment.
Dev servers on this machine whose URL isn't spelled as loopback will now hit the "External website preview" warning. Details inline.
…back Review on #1474: the gate called two kinds of upstream on this machine external. `python -m http.server` and other servers bound to every interface print http://0.0.0.0:<port>/, and listenerUrlsByPort names a server bound to one LAN, bridge, or tailnet interface by that interface's address. Both connect here, and both now opened behind the external-website warning. isLoopbackAddress becomes isThisMachineAddress: loopback, the unspecified 0.0.0.0 and ::, or an address one of this host's interfaces holds, read from os.networkInterfaces() at mint (injectable for tests), IPv4-mapped spellings counting as their IPv4. A private address on another host stays external. The spec's definition of local, S294, and the rationale name the new set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve scripts/spec-word-budgets.json by taking the larger budget per spec. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve scripts/security-rule-ids.json to next 295 with only 265 retired, since #1459 now uses S292 and S293 and this branch uses S294. Resolve scripts/spec-word-budgets.json by taking the larger budget per spec. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #1487's story-clock fix for the access-log story, which is the one changed Argos snapshot every open PR showed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1515 added tunneled phone grants to the same proxy, so the merge reconciles the two gates in mintGrant: one locality check, refusing a tunneled grant first, then an external one the user has not allowed. - Pass allowExternal and tunneled to the VS Code host in an options object. Both were the fourth positional argument, so a plain merge let the webview's flag set tunneled. - Keep main's isThisMachineAddress and drop this branch's copy. - Skip the off-origin redirect page on a tunneled grant, whose Pocket frame cannot follow it. - Renumber this branch's rule to S299, since main's S294 is the frame tunnel, and set next to 300. - Ratchet docs/specs/dor-browser.md, since both sides added words. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dormouse-bot
left a comment
There was a problem hiding this comment.
The merge from main (e8b837e) stopped handleRequest from serving the off-origin redirect page on a tunneled grant, but the spec and S299 still describe that page as covering every frame load with an embedder chain. Tunneled grants always have a chain, because createBurrowFrameHost refuses to open one without it. So the nightly audit will read the !grant.tunneled check as a violation of S299, and a reader of "Iframe External Origins" won't learn that Pocket frames get the Location unchanged. Suggestions inline. If the exemption is better owned by docs/specs/remote-api.md -> "Frames", point there instead.
…he phone #1515 sends a phone's frames through the same proxy, on tunneled grants that are refused off this machine whatever allowExternal says. The merge made the code agree; this pins it and says so. A tunneled grant now has a test that an off-origin redirect passes as sent: PocketBrowserFrame handles no `navigate`, so the Dormouse page would be a dead end there. burrow.test.ts pins the VS Code glue asking the real proxy for a phone's frame as tunneled, through the argument shape that once collided with allowExternal. S299 and the Guarantee row name local panes and point at S294 for tunneled grants; dor-browser.md defers its definition of local to remote-api.md's, which also stops saying a local pane keeps every remote page. The phone's redirect is recorded in remote-api.md's Future. pinAddresses' comment no longer claims every private address is trusted, which stopped being true when the gate landed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep both sides of vscode-adapter.ts's imports: IframeProxyOptions from this branch and TERMINAL_CONTEXT_TIMEOUT_MS from main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Iframe panes now ask before previewing a website from another machine. Before this, the proxy fronted any
http://upstream, stripping a remote site's framing headers and CSP and serving it from the shared loopback hostname, with no warning. A frame load redirected off its upstream origin was forwarded as sent, and the webview's loopback-onlyframe-srcrefused it, leaving a blank pane.What it does
mintGrantrefuses an upstream off this machine asexternalunless the request'sallowExternalis exactlytrue, judged on the addresses the grant pins and never on the name. It uses the sameallOnThisMachinecheck Show pages and Tools in Pocket over the encrypted session #1515 added. A leased external grant is not reused once the allowance is withdrawn.localStorage, listed and removable in Settings → General. Allowing or removing one re-asks the host, so a removal closes an open preview.navigateto the app. The panel takes it as a URL-bar entry through the same gate, replacing the redirecting history entry. A frame nested in a proxied page follows the redirect itself, outside the webview CSP.S299indocs/specs/security-local.md-> "Browser panes", with a Guarantee row. Thelocal-terminalstream of.github/audit/local.mdalready claims that heading, so the prompt is unchanged.Allowing an origin permits a preview only: cookies stay stripped and
https://stays refused. Following an off-proxy link or form in place, and cookie support, stay under## Futureindocs/specs/dor-browser.md.How this meets #1515
#1515 sends a phone's frames through this same proxy on tunneled grants. This PR gates local panes only:
allowExternalsays, and that refusal runs first. That rule staysS294indocs/specs/security-remote.md;S299points at it.PocketBrowserFramehandles nonavigate, so the Dormouse page would be a dead end on a phone. What a phone should do instead is recorded indocs/specs/remote-api.md-> "1b. A frame redirected off its page", for a later PR.docs/specs/remote-api.md-> "1a. Frames off this machine" already says a webview's allowlist cannot be a phone's consent, so a phone framing remote pages would need its own.createIframeProxyUrltakes an options object, soallowExternalandtunneledcannot be confused by position.vscode-ext/test/burrow.test.tsasks the real proxy through the glue and pins the refusal.Testing
IframePanel, Settings, the VS Code router and Burrow glue, and the standalone adapters.storybook-chromium, andcargo checkpassed before the Show pages and Tools in Pocket over the encrypted session #1515 merge; the Rust command has not changed since.tscpasses in lib, standalone and vscode-ext, as do the specs, md, public-docs, outbound and loopback lints.true, the tunneled-first refusal, the tunneled redirect pass-through, and the glue'stunneled.Not verified: the warning, allowlist and redirect in a live VS Code or Tauri window, and anything on a real phone.
Fixes #572
🤖 Generated with Claude Code