web: enforce per-app _meta.ui.csp + surface resource-read failures#1652
web: enforce per-app _meta.ui.csp + surface resource-read failures#1652cliffhall wants to merge 4 commits into
Conversation
…e-read failures - createAppBridgeFactory now builds each app's `_meta.ui.csp` host-side via the sandbox-csp library (approveCspSources → buildSandboxCspPolicy) and wraps the untrusted widget HTML with wrapSandboxedHtml so the CSP <meta> is always the first <head> child before the HTML reaches the sandboxed iframe. The approved (post-filter) csp + permissions are echoed back via hostCapabilities.sandbox so the view sees what was granted. The csp is no longer sent inline through sendSandboxResourceReady — it is enforced via the wrapped meta. - A per-app copy of HOST_CAPABILITIES prevents the sandbox echo from mutating the shared constant. - New optional `onResourceError` dep on AppBridgeFactoryDeps routes resource read/post failures to the caller (also always console.error'd) instead of leaving a silent blank frame. Closes #1566 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
|
@claude review |
|
Claude finished @cliffhall's task in 4m 15s —— View job Review: per-app CSP enforcement + resource-error surfacing
Overall this is a clean, well-documented change. The host-side CSP construction ( 1. The sandbox echo depends on
|
Address the claude[bot] review's finding #2: add a comment explaining why `csp` is injection-filtered (approveCspSources) while `permissions` is passed through verbatim — permissions is a structured boolean object consumed only by the sandbox proxy's buildAllowAttribute() (fixed key→token mapping), so it never reaches the iframe sandbox/allow attribute as raw text. No logic change. Also merges v2/1565-sandbox-hardening (review fixes) up the stack. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
|
Thanks — great catch prioritizing #1. Responses: 1. AppBridge reads
So mutating 2. Permissions echoed/forwarded unfiltered while CSP is filtered — applied (comment). Added a comment at the mutation site (345395b) explaining the asymmetry: 3. Nits (extractHtmlAndMeta first-text-block, test naming) — acknowledged, no change; the empty/no-text case is the important one and is covered.
|
|
@claude review |
|
Claude finished @cliffhall's task in 1m 15s —— View job Re-review: per-app CSP enforcement + resource-error surfacing
The three points from the earlier review are all addressed, and the added asymmetry comment (345395b) is accurate against the
The host-authored shell ( One nit — the per-app-copy isolation isn't locked in by a test
import { createAppBridgeFactory, HOST_CAPABILITIES } from "./createAppBridgeFactory";
it("does not mutate the shared HOST_CAPABILITIES when echoing the approved sandbox", async () => {
const readResource = vi.fn().mockResolvedValue(
uiResource("<h1>x</h1>", { csp: { connectDomains: ["https://api.example.com"] } }),
);
const factory = createAppBridgeFactory({ getClient: () => fakeClient, readResource });
await factory(makeIframe(), tool);
bridgeInstances[0].emit("sandboxready");
await flush();
expect(HOST_CAPABILITIES.sandbox).toBeUndefined();
});Non-blocking — the current behavior is correct; this just prevents a silent regression of the exact bug the copy guards against. Fix this → Everything else — the CSP wrapping, unsafe-source drop, echo, and the three error branches ( |
Add a test asserting the shared HOST_CAPABILITIES.sandbox stays undefined after a sandboxready run sets hostCapabilities.sandbox — guards against a silent regression if the per-app spread copy were ever dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
|
Applied the nit in a follow-up commit: added |
Closes #1566
Wires per-app CSP enforcement into the app bridge and surfaces resource-read failures (re-implementation of the relevant slice of PR #1510).
Changes
createAppBridgeFactorybuilds each app's_meta.ui.csphost-side via the sandbox-csp library (approveCspSources→buildSandboxCspPolicy) and wraps the untrusted widget HTML withwrapSandboxedHtml, so the CSP<meta>is always the first<head>child before the HTML reaches the sandboxed iframe. The approved (post-filter) csp + permissions are echoed back throughhostCapabilities.sandbox. The csp is no longer sent inline viasendSandboxResourceReady.HOST_CAPABILITIESkeeps the sandbox echo from mutating the shared constant.onResourceErrordep routes read/post failures to the caller (alwaysconsole.error'd too) instead of a silent blank frame.Error display in AppsScreen (
data-app-error) is intentionally out of scope here — it belongs to the app-status issue (#1572). This PR only surfaces the error via the factory dep.Tests
onResourceError/console.error on read failure, no-text content, and non-Error rejection.npm run cipasses locally.Stacking
Stacked on #1565 (
v2/1565-sandbox-hardening). Review/merge that first.🤖 Generated with Claude Code
https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw