fix(sdk-ui): decode screenshots via createImageBitmap so host CSPs can't block capture - #6
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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 againstimg-src. Under such a policy the load is refused,errorfires instead ofload, and the error handler only revoked the URL — it never setbgnor closed the reporter. The wizard is gated onbg(reporter.tsx), so it sat on the overlay indefinitely withbody.overflow = "hidden"locking the page behind it.script-src/connect-srcon 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, soimg-srcnever 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 theBlob → drawablehop moved.Also in here:
<canvas>(BlobImage) instead of<img src="blob:">. The same CSP broke these silently (broken thumbnails rather than a hang). CSS selectors widened, sinceobject-fit/aspect-ratioapply to<canvas>as a replaced element.<img>fallback is bounded by a timeout.bgis nowImageSource, which let theas unknown as { naturalWidth?: number }casts incanvas.tsx,flatten.tsandstep-annotate.tsxgo away.previewUrlstays on the sharedAttachmenttype — the Expo SDK carries afile://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:
decode-image.test.tslocks in "decodes without minting a blob: URL" and "returns null instead of hanging".attachment-list.test.tsnow asserts the thumbnail renders from the blob withpreviewUrlabsent.bun run checkclean (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:So the extension hit the same hang — on exactly the CSP-strict sites it exists to inject into. It already proxies
fetcharoundconnect-src, but there was no equivalent forimg-src. It bundles@reprojs/coreat 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/coreon 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