diff --git a/AGENTS.md b/AGENTS.md index 06af20e5c..d3d5f79a0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -984,7 +984,7 @@ These surfaces are **comment-only**. `redline` (auto-DELETION) and `quickLabel` *Security.* Embedded pages are untrusted author HTML served from the Plannotator server's origin. Inside the annotate surface the primary document's `sandbox="allow-scripts"` iframe already protects them: sandboxing flags are inherited and intersected by every nested browsing context, so an embed runs at an opaque origin with no `allow-same-origin` and its `fetch('/api/plan')` fails as cross-origin (verified in the browser, before AND after this change — the pre-fix nested Plannotator was already origin-`null`, which is exactly why the owner saw "no cookies in it"). Defense in depth for the case with no parent sandbox — a reviewer pasting an asset URL into a top-level tab — is on the response: every HTML response from the asset route carries `Content-Security-Policy: sandbox allow-scripts` (`HTML_ASSET_DOCUMENT_CSP`, never `allow-same-origin`) plus `X-Content-Type-Options: nosniff`, and no cookies are involved. Nothing widens what is readable: the same per-directory token, the same `..` refusal, the same `isWithinDirectory` symlink check; HTML documents additionally honour the 2MB `MAX_ANNOTATABLE_FILE_BYTES` annotate cap rather than the 50MB asset cap. The decision is single-sourced in `resolveHtmlAssetRoute` (`packages/shared/html-assets.ts`, vendored to Pi) so the Bun route and the Pi mirror cannot drift. -*Never the app in a frame.* A request whose `Sec-Fetch-Dest` is `iframe`/`frame`/`embed`/`object` — or any `.html` path under the assets prefix — gets a small plain 404 document naming the missing file instead of the catch-all app or a JSON blob, in both runtimes. Ordinary asset misses keep their JSON shape. +*Never the app in a frame — but only for a path that could BE a file.* Under the assets prefix, a request whose `Sec-Fetch-Dest` is `iframe`/`frame`/`embed`/`object`, or any `.html` path however it was made, gets a small plain 404 document naming the missing file instead of a JSON blob; ordinary asset misses keep their JSON shape. The annotate **catch-all** applies the same 404 document, but only when BOTH conditions hold (`isFramedEmbeddedDocumentRequest` = `isFramedFetchDest` && `pathNamesEmbeddedDocument`, `packages/shared/html-assets.ts`, vendored to Pi and used by both runtimes): the destination is framed, AND the path is not `/` and either its last segment carries an extension (`/prototype-slash.html`) or it sits under a directory segment (`/assets/frame`). `/` and a bare single-segment word (`/settings`) are served the app as always, so a framed session URL and any future SPA route cannot 404. The rule keys on the **shape of the path, not `Sec-Fetch-Site`**: an annotated page is a sandboxed srcdoc with an opaque origin, so its nested-document requests are `cross-site` — the same value the VS Code webview wrapper produces, and `none` on both sides for a pasted URL — so site can never separate the two, while the path can, because the app only ever loads at `/` and a relative embed is anchored at `/api/html-assets//` by the `` (with its own 404); only a root-relative embed reaches the catch-all at all. Scoping it this way is the #1561 regression fix: the VS Code extension renders the session URL inside an `