Implementation review: PB-006, PR 250 (and 445)
| Field | Value |
|---|---|
| Backlog item | PB-006 Content Security Policy for the main window (feature FEAT-SEC-02; hardening HD-020) |
| Pull request | #250, merged as 29f76070 (head 5c3fc95f; work in ec311614, f8972ae0), set the CSP and moved notebook HTML output to the nbout: frame. #445, merged as aef0d50f (head 38af2939), added the browser test that runs the UI under that CSP. |
| Package | None (no docs/product/pr-ready/PB-006.md; built as HD-020 of the hardening backlog) |
| Reviewer | Engineering quality review thread. It didn't write #250 or #445. |
| Date | 2026-10-27 |
| Environment | Linux container, main 6159e507, Python venv for the engine, Playwright Chromium. No tenant, no Windows, no second PC. |
Outcome#
Accepted with Documented Limitations (recommended).
The CSP is set, the UI and CSP tests pass, and they fail when the CSP is removed. Notebook HTML output runs in a sandboxed frame with an opaque origin and its own policy with no connect-src. A probe in Chromium found it can't reach the parent page, the IPC endpoint or the network. Two things are open. The WebView2 part is only checked by reading the code. And docs/security.md says the frame "never receives the shell's invoke key", but the Tauri source this build uses puts the invoke key in every frame on Windows (finding 1). The boundary still holds, but on other layers than the one the docs name.
Recommended by: Engineering quality review. Decided by (product owner): pending.
Acceptance criteria#
| # | Criterion (copied from the backlog item) | Result | Evidence |
|---|---|---|---|
| 1 | A CSP is set and the UI test suite passes | Met | app/src-tauri/tauri.conf.json:21-33: default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src ipc: http://ipc.localhost; frame-src nbout: http://nbout.localhost https://nbout.localhost; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'. node --test app/tests/ui.test.cjs: 297 tests, 297 pass. app/tests/browser/csp.test.cjs (#445) serves the UI with this policy: 11/11 pass (inline script blocked and reported; home, create, monitor, settings, notebooks, workspaces, optimize, profile, the notebook editor and the SQL editor open with no violations). |
| 2 | Notebook HTML output can't call Tauri commands | Met (code reading and Chromium); WebView2 not checked | htmlFrame (app/ui/notebook.js:1030-1035) builds <iframe sandbox="allow-scripts"> with no allow-same-origin, loaded from nbout (main.rs:1998-2025, registered at main.rs:2116, 403 for any webview other than main). The frame page's header policy (main.rs:2000-2002) has default-src 'none' and no connect-src. All four HTML paths go through it: type: "html" (notebook.js:983), Altair (1062), Plotly (1087), and the ipywidgets HTML widget (ipywidgets.js:55). The app page's only message listener reads a number for the frame height (notebook.js:1092-1097). Reviewer probe (below): from the frame, parent.document, parent.__TAURI__ and parent.rpc throw SecurityError, self.origin is "null", fetch("http://ipc.localhost/rpc") and a fetch to another host are blocked (no request leaves the page), and top.location = … throws SecurityError. In Tauri 2.12.2, an IPC request with Origin null is refused (tauri ipc/protocol.rs:486-494). |
Checks#
| Check | Result | Evidence or note |
|---|---|---|
| Scope matches the item | Met | It matches the item's plan: HTML output moved to another origin (nbout:), then a CSP was set. withGlobalTauri is still true (tauri.conf.json:9). The item doesn't ask to remove it, and security.md:35 says a script that did run in the app's page could call the API. |
| New tests fail without the change, pass with it | Met | Copy of main with csp: null and the pre-#250 srcdoc htmlFrame: ui.test.cjs test 100 ("the app has a strict CSP and notebook HTML output stays off the app's origin") fails with "tauri.conf.json must set app.security.csp", and csp.test.cjs fails 11/11 (test 1: the inline script ran, true !== false). The copy had no docs/, so 59 docs-reading ui tests also failed there; those don't bear on this item. With main's files, all pass (above). |
| Full suites pass (pytest, UI, cargo) | Partly | ui.test.cjs 297/297. Browser tests that cover HTML output: altair 1/1, plotly 1/1, ipywidgets 1/1, notebook-runs 3/3, csp 11/11. cargo isn't buildable here. The Rust test notebook_output_frame_has_no_network_and_only_opens_in_the_app (main.rs:2248-2257) checks that the frame policy has no connect-src or http source and gives the Fabric view a 403; CI runs cargo test on windows-latest (.github/workflows/ci.yml:281). Not run here. No engine change. |
| Reachable where docs say; Preview behavior right; packaged into the engine | Met | No new UI; notebook HTML, Altair, Plotly and widget HTML render in the frame (browser tests above). |
| OneLake data safety | n/a | UI and shell only. |
| Authentication and distribution | Met with a note | No new scope or command. Tauri treats a registered custom protocol host (nbout.localhost on Windows) as a local origin (tauri webview/mod.rs:1961-2000), and the app has no app ACL manifest (build.rs), so any IPC that did arrive from the frame would be allowed to call rpc. What stops it: the frame's opaque origin (Origin null is refused), the frame policy (no connect-src, so the fetch to ipc.localhost never leaves), and WebView2 sending an iframe's chrome.webview.postMessage to the frame's own event, which wry 0.57 doesn't subscribe to (wry webview2/mod.rs:956 listens on CoreWebView2.WebMessageReceived only). The last one is WebView2 behavior and isn't checked here. |
| No unproven claim; docs honest about limits | Partly | security.md says plainly that WebRTC can still leak a notebook's own output. Its claim that the frame "never receives the shell's invoke key" doesn't match Tauri on Windows (finding 1). |
| Docs match the code; linked; strict docs build passes | Partly | security.md "Content Security Policy" quotes the policy exactly as tauri.conf.json has it. The invoke key sentence is wrong for Windows (finding 1). The strict docs build wasn't run here. |
| Accessibility of new controls | n/a | No new controls; the frame keeps title="HTML output". |
Inventory and backlog updated; product_data.py consistent |
Partly | FEAT-SEC-02 is Implemented / Partially Verified with "check under WebView2 on Windows" (FEATURE_INVENTORY.md:284, 1850-1862). docs/hardening/backlog.md:130 says the Windows check was "checked by hand and recorded in the PR"; there was no GitHub access here to see that record. PB-006 is still "In review" (PRODUCT_BACKLOG.md:1386). |
Reviewer probe: $S/pb006/app/tests/browser/zz-probe.test.cjs (scratchpad copy of main, not in the repo). It opens the app with the shell's CSP (csp: true), serves the frame page with OUTPUT_FRAME and OUTPUT_CSP read from main.rs, and renders {type: "html"} output whose script tries each escape and posts the results back. Output: {"parentDoc":"blocked: SecurityError","parentTauri":"blocked: SecurityError","parentRpc":"blocked: SecurityError","origin":"null","ipcFetch":"blocked: TypeError","netFetch":"blocked: TypeError","topNav":"blocked: SecurityError","ownTauri":"undefined"}. The only request outside the app was the frame page itself. 1 pass. ownTauri says nothing about Windows, because the test harness puts no Tauri script in the frame.
Findings#
| # | Severity (blocking / should fix / note) | Finding | Where | Status |
|---|---|---|---|---|
| 1 | should fix | security.md says the output frame "never receives the shell's invoke key, so it can't call the Tauri API". Tauri 2.12.2 marks its IPC and invoke key scripts for_main_frame_only, but wry 0.57 says that on Windows "scripts are always added to subframes regardless of the for_main_frame_only option" (wry lib.rs:1029, webview2/mod.rs:506-508, AddScriptToExecuteOnDocumentCreated). So on WebView2 the frame probably has __TAURI_INTERNALS__ and the key. What actually stops it is the opaque origin, the frame policy with no connect-src (guarded by the Rust test) and WebView2's per-frame message routing. The docs should say that. |
docs/security.md "Content Security Policy" section | Open: docs owner |
| 2 | should fix | No committed test renders type: "html" output and checks that it can't reach the parent or the IPC endpoint. The ui.test only matches the htmlFrame source text; the Altair and Plotly tests only check that nothing is fetched. A test like the reviewer probe would catch an allow-same-origin, a connect-src added to the frame policy, or a new listener in the app page. |
app/tests/browser | Open |
| 3 | should fix | On Windows (WebView2), nobody here checked that script in the output frame can't call invoke: the frame does get Tauri's scripts, and nbout.localhost counts as a local origin, so the defence rests on WebView2's frame handling. A short check: a %%html cell with <script>__TAURI_INTERNALS__.invoke("check_components").then(r=>document.body.textContent="REACHED", e=>document.body.textContent="refused "+e)</script>, which should not show REACHED. |
Windows build | Open: Chris, FEAT-SEC-02 next action |
| 4 | note | withGlobalTauri: true stays, so anything that did run in the app's page could call the API. The CSP (script-src 'self') and escaping are the layers in front of it, as security.md:35 says. Notebook Markdown is still rendered with innerHTML in the app's page (app.js:2580), under that CSP. |
tauri.conf.json:9 | Known; outside this item |
Documented limitations (only for Accepted with Documented Limitations)#
| Limitation | Where users are told | Backlog item that tracks it |
|---|---|---|
| WebRTC isn't covered by a CSP, so a notebook written to leak its own output could send it out that way. | docs/security.md (threat table, Content Security Policy section) | FEAT-SEC-02 known limitations |
| The Tauri API is still a page global in the app's own page. | docs/security.md:35 | HD-020 follow-ups |
| Not checked under WebView2 on Windows by this review. | FEAT-SEC-02 next action ("check under WebView2 on Windows") | FEAT-SEC-02; finding 3 |
Inventory and backlog changes#
- Features changed: FEAT-SEC-02: add a known limitation that the frame's protection on Windows rests on the opaque origin and the frame policy, not on the frame lacking the invoke key (finding 1). Keep Partially Verified until the Windows check (finding 3).
- Backlog item state: PB-006 In review → Accepted with Documented Limitations, on the product owner's decision.
Need a hand?
Find a fix in troubleshooting, or share a reproducible issue with the community.
Get support ↗Improve this guide on GitHub ↗