Skip to content

fix(sdk-ui): decode screenshots via createImageBitmap so host CSPs can't block capture - #6

Merged
Ripwords merged 2 commits into
mainfrom
fix/csp-blob-screenshot-decode
Jul 17, 2026
Merged

fix(sdk-ui): decode screenshots via createImageBitmap so host CSPs can't block capture#6
Ripwords merged 2 commits into
mainfrom
fix/csp-blob-screenshot-decode

Conversation

@Ripwords

Copy link
Copy Markdown
Owner

The bug

Reported from a live embed: after accepting the native "Share this tab?" prompt, the page appeared to freeze on a "Capturing…" overlay forever.

Root cause is the host page's CSP:

img-src 'self' data: …        # no blob:

The reporter turned the captured screenshot into a blob: object URL and assigned it to an <img>. That's a resource load, so it's checked against img-src. Under such a policy the load is refused, error fires instead of load, and the error handler only revoked the URL — it never set bg nor closed the reporter. The wizard is gated on bg (reporter.tsx), so it sat on the overlay indefinitely with body.overflow = "hidden" locking the page behind it.

script-src/connect-src on that site both allow the dashboard origin, which is why loading and uploading worked and only the image broke.

The fix

Decode the Blob with createImageBitmap — an in-process decode with no fetch, so img-src never applies, on any host CSP. No embedder config needed, which keeps the zero-config embed promise.

The native capture path is unchanged. getDisplayMedia, the "Share this tab?" prompt, preferCurrentTab, ImageCapture.grabFrame() all still run. Only the Blob → drawable hop moved.

Also in here:

  • Preview + thumbnails render from the blob via <canvas> (BlobImage) instead of <img src="blob:">. The same CSP broke these silently (broken thumbnails rather than a hang). CSS selectors widened, since object-fit/aspect-ratio apply to <canvas> as a replaced element.
  • No more silent hangs. A decode failure now closes the reporter with a warning, and the <img> fallback is bounded by a timeout.
  • Types. bg is now ImageSource, which let the as unknown as { naturalWidth?: number } casts in canvas.tsx, flatten.ts and step-annotate.tsx go away.

previewUrl stays on the shared Attachment type — the Expo SDK carries a file:// uri in it (provider.tsx). Only the web path stops using it.

Verification

Replayed the reported site's real CSP against a built bundle in Chromium with tab-capture auto-accepted:

Before After
Annotate step "Capturing…" overlay forever real screenshot + toolbar
Details step n/a (unreachable) preview renders, form intact
  • 369 SDK tests pass; new decode-image.test.ts locks in "decodes without minting a blob: URL" and "returns null instead of hanging".
  • attachment-list.test.ts now asserts the thumbnail renders from the blob with previewUrl absent.
  • bun run check clean (0 errors).

Note: the Chrome extension is affected too

The extension loads the SDK via chrome.scripting.executeScript({ world: "MAIN" }), which gets no CSP exemption. Measured on a CSP page:

blob: <img> in MAIN world     -> BLOCKED by page CSP
blob: <img> in ISOLATED world -> LOADED (CSP bypassed)

So the extension hit the same hang — on exactly the CSP-strict sites it exists to inject into. It already proxies fetch around connect-src, but there was no equivalent for img-src. It bundles @reprojs/core at build time (sync-sdk.ts), so it picks this up on its next build + release.

Release plan

Three patch releases needed, since each artifact ships the bundle separately:

  • sdk-v0.4.2@reprojs/core on npm (npm embedders)
  • v0.6.5 → dashboard Docker image, which bakes and serves /sdk/repro.iife.js (fixes the reporting site)
  • extension-v0.1.4 → Chrome Web Store

🤖 Generated with Claude Code

Ripwords and others added 2 commits July 17, 2026 11:25
…n't block capture

Host pages commonly ship `img-src 'self' data:` with no `blob:`. The reporter
turned the captured screenshot into a blob: object URL and assigned it to an
<img>, which is a resource load and so is checked against img-src. Under such
a policy the load was refused, `error` fired instead of `load`, and the error
handler only revoked the URL — it never set `bg` nor closed the reporter. The
wizard is gated on `bg`, so it sat on "Capturing…" indefinitely with the page
scroll locked behind the overlay.

Decode the Blob with createImageBitmap instead: an in-process decode with no
fetch, so img-src never applies. The native getDisplayMedia capture path is
unchanged — only the Blob → drawable hop moves.

Also:
- Render the details preview and attachment thumbnails from the blob via
  <canvas> (BlobImage). Same CSP refused those too, silently.
- Close the reporter on a decode failure, and bound the <img> fallback with a
  timeout, so no input can strand the loading overlay again.
- Type bg as ImageSource, dropping the `as unknown as { naturalWidth? }` casts
  in canvas.tsx, flatten.ts and step-annotate.tsx.

previewUrl stays on the shared Attachment type — the Expo SDK carries a
file:// uri in it. Only the web path stops using it.

Verified by replaying the reported site's CSP against a built bundle: before,
the overlay hung forever; after, the wizard reaches annotate with a real
screenshot and Details renders its preview.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Serve the demo page at /csp behind the real host-page policy that triggered
the "Capturing…" hang — `img-src 'self' data:` with no `blob:`. Capture has
to keep working there, which is what createImageBitmap buys us; the old
blob:-URL path hung on that route.

/ stays as-is for a no-CSP baseline, so the two can be compared side by side.
Also injects a well-formed dummy project key so the screenshot flow runs
without editing index.html — capture is client-side, so no real key is
needed unless you want to submit (set REPRO_DEMO_KEY for that).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Ripwords
Ripwords merged commit 7a7e59b into main Jul 17, 2026
12 of 13 checks passed
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.

1 participant