Skip to content

Harden iframe previews: press reports, service workers, and metadata addresses - #1537

Merged
nedtwigg merged 13 commits into
mainfrom
iframe-focus-and-proxy-hardening
Nov 3, 2026
Merged

nedtwigg merged 13 commits into
mainfrom
iframe-focus-and-proxy-hardening

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Three pieces of hardening in the iframe preview.

A frame's press report counts only from the frame holding focus. The shim posts pointerdown so the Wall can enter a pane it cannot see clicked. The page shares the shim's origin and can post the same message, so IframePanel now acts on it only when document.activeElement is its own frame. A report from any other frame moves nothing: not focus, selection, or mode, and the terminal keeps the keyboard. The shim focuses its own frame before it posts, so a real press still enters the pane, including on a page that cancels mousedown or pointerdown, which a browser gives no focus. A new S303 holds this. docs/specs/security.md -> "Known gaps" gains a line for what no parent can stop: a browser lets a frame focus itself.

No service workers on a local preview. A worker stays with its origin after the grant ends and would answer the next grant on that port ahead of the proxy and its Clear-Site-Data. The proxy now answers a service worker script fetch with 403 on every grant but a tunneled one. A local PWA previews without its worker.

The metadata refusal covers the addresses it claims. isBlockedAddress now refuses the fixed cloud metadata addresses outside 169.254/16, and reads IPv6 as bytes so a metadata address named by, or reached through a translator from, an IPv6 literal (mapped in any spelling, compatible, translated, NAT64) is refused however it is written. Tunnel forms (6to4, Teredo, ISATAP) are not read, since a tunnel ends at a host that decapsulates IPv6, not at an HTTP metadata service. ipv6Bytes shares its parser with the network policy's address math, which moved to lib/src/lib/ip-literal.ts.

Testing

  • pnpm test at the root and tsc -b in lib pass. Each new test goes red with its fixed line removed.
  • In the real Wall, on Chromium through innerdogfood: a report with no click leaves the terminal focused and typing in it; a real click enters the frame, selection follows, and keys reach the page; the same on a page that cancels the press; moving between frame and terminal works both ways.
  • Press timing: focus reached the embedding document ahead of the report on every press, 200 of 200 in out-of-process Chromium at one level and at VS Code's depth, and 40 of 40 in a macOS WKWebView.
  • A worker fails to register on a grant in real Chromium, with the 403.

Not run: the shipped Tauri and VS Code apps, WebView2, and WebKitGTK.

🤖 Generated with Claude Code

nedtwigg and others added 6 commits November 2, 2026 19:42
The press report a proxied frame posts now selects the pane in passthrough
and acknowledges its Session without moving DOM focus: the page shares the
shim's origin, so it can post the same message with no click. The shim takes
focus from inside for a real press, which also covers a page that cancels the
press. A frame out of sight, which no one can click, is not heard, so selecting
it cannot hand it focus when its Workspace comes back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…frame proxy

The address guard now covers the fixed cloud metadata addresses outside
169.254/16 and reads IPv6 literals as bytes, so a metadata address carried
through a transition form (mapped in any spelling, translated, NAT64, 6to4,
Teredo, ISATAP) is refused however it is written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A worker registered on a grant's origin outlives the grant and answers the
next grant on that port ahead of the proxy and its Clear-Site-Data. The proxy
now answers a service worker script fetch with 403 on every grant but a
tunneled one, whose page is not at that port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Focus reaches the embedding document ahead of the report, so testing it needs
no new Wall action or visibility gate: a report from a frame without focus now
moves nothing, including selection and the terminal's focus. Share one IPv6
parser between the address guard and the network policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3bc04b3
Status: ✅  Deploy successful!
Preview URL: https://e3fdde33.mouseterm.pages.dev
Branch Preview URL: https://iframe-focus-and-proxy-harde.mouseterm.pages.dev

View logs

nedtwigg and others added 2 commits November 2, 2026 20:28
…y-hardening

# Conflicts:
#	scripts/security-rule-ids.json
#	scripts/spec-word-budgets.json
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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.

The 6to4, Teredo, and ISATAP branches of ipv4sCarriedByIPv6 refuse addresses that cannot reach a metadata endpoint, so they add bit math and tests for no trigger. Those three are tunnels, not translators. A packet to 2002:a9fe:a9fe:: is sent to 169.254.169.254 as IPv6 inside IPv4 protocol 41 (RFC 3056 §5), and ISATAP does the same (RFC 5214). A Teredo address is reached over UDP at the client's mapped address (RFC 4380), and the server address in the prefix never carries data. The IPv4 host at the embedded address has to decapsulate and speak IPv6 itself, and an IMDS HTTP endpoint doesn't, so no TCP connection to it on port 80 ever forms. Only translation (mapped/SIIT, NAT64 at any of its prefix lengths) ends an IPv6 connection at an IPv4 service. Those branches are the ones that hold the guard, and they stay.

Dropping the tunnel branches also fixes the claim in the test comment ("hands the embedded address to a router or translator, so the connection ends at the IPv4 address") and in dor-browser.rationale.md, which currently say something the network doesn't do. The PR description's "(… 6to4, Teredo, ISATAP)" would need the same edit. The suggestions below cover the code, tests, and rationale. The '2001:db8::5efe:808:808' row in the does-not-block table can stay or go.

Checked: the four touched test files pass locally (390 tests).

Comment thread lib/src/lib/ip-literal.ts Resolved
Comment thread lib/src/lib/ip-literal.ts Resolved
Comment thread docs/specs/dor-browser.rationale.md Resolved
6to4, Teredo, and ISATAP are tunnels: they carry IPv6 inside IPv4 to the
embedded address, which has to decapsulate and speak IPv6 itself, so no TCP
connection to an HTTP metadata endpoint forms. The guard keeps the forms that
name an IPv4 address or reach one through a translator, and the comments and
rationale now say that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dormouse-bot
dormouse-bot previously approved these changes Nov 3, 2026

@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.

Build & Test is red at this head because of the spec-lint problem the description names: scripts/security-rule-ids.json: S302 is in no security spec and not retired. That clears only once #1535 lands and main is merged in here, so I'm withdrawing the approval until then. The code change itself has no open findings.

@dormouse-bot
dormouse-bot dismissed their stale review November 3, 2026 04:49

CI failed — spec-lint S302 pending #1535

…y-hardening

# Conflicts:
#	scripts/spec-word-budgets.json
…y-hardening

# Conflicts:
#	scripts/spec-word-budgets.json
…y-hardening

# Conflicts:
#	scripts/security-rule-ids.json
#	scripts/spec-word-budgets.json
…y-hardening

# Conflicts:
#	scripts/spec-word-budgets.json
@nedtwigg
nedtwigg merged commit 1221b80 into main Nov 3, 2026
19 checks passed
@nedtwigg
nedtwigg deleted the iframe-focus-and-proxy-hardening branch November 3, 2026 07:04
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