Skip to content

Make iframe previews local-first, with an external-origin warning and allowlist - #1474

Draft
nedtwigg wants to merge 6 commits into
mainfrom
issue-572-iframe-local-first
Draft

nedtwigg wants to merge 6 commits into
mainfrom
issue-572-iframe-local-first

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Make iframe previews local-first, with an external-origin warning and 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 reaching this machine: loopback, 0.0.0.0 / ::, or an address one of this host's interfaces holds; 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.

Add a Storybook story for the external website preview warning

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.

Audit the iframe external-origin gate as FAIL IF S294

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.

Defaults confirmed by the maintainer: Open in browser uses the system browser, and the allowlist is kept per install. The gate is audited as S294.

Merge with #1459: scripts/security-rule-ids.json will conflict. Keep next: 295 and remove 292 and 293 from retired, since #1459 uses them as live rules (this branch retires them only to satisfy the ledger lint on its own).

Fixes #572

🤖 Generated with Claude Code

nedtwigg and others added 3 commits October 30, 2026 13:30
… 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>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0ec6a54
Status: ✅  Deploy successful!
Preview URL: https://43180a6c.mouseterm.pages.dev
Branch Preview URL: https://issue-572-iframe-local-first.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dev servers on this machine whose URL isn't spelled as loopback will now hit the "External website preview" warning. Details inline.

Comment thread lib/src/host/iframe-proxy-rewrite.ts Resolved
nedtwigg and others added 3 commits October 30, 2026 16:20
…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>

This branch has not been deployed

No deployments
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.

Make iframe previews local-first with an external-origin warning and allowlist

2 participants