feat: gate remote images in markdown preview behind a default-off pref (#69)#85
Conversation
simion#69) The markdown preview used to render any  image directly, which meant an unprompted GET could fire to whatever host untrusted content named the moment a file was previewed - prompt injection, a dependency's README, a contributor's fork - since the webview sits outside the seatbelt + CONNECT proxy cage. Accepted as a known gap in bca4e13, tracked for closing in simion#69. This gates that hydration behind a default-off pref, with two escape hatches. gateRemoteImages() in MarkdownPreview.tsx now intercepts every http(s) <img> src before it ever reaches the DOM's src attribute, so the browser never issues the request when blocked. Gated on prefs.loadRemoteImages (a new pref, default OFF) or a per-tab remoteImagesUnblocked override. The CSP itself is unchanged (still allows https: in img-src) - this is a renderer gate, not a CSP tweak, since Tauri's CSP is one whole-webview policy that can't scope to a single component. Two escape hatches, surfaced as a banner in the preview: "Show images" unblocks just the open document, session-only (dies with the tab, like mdView's per-tab override). "Always" flips the global pref - the same banner swaps its text/action in place to a "Settings" link, which opens Settings, lands on General, and auto-scrolls to + briefly highlights the toggle. Also fixes a bug caught during manual testing: openPreviewTab's "recycle this preview tab into a different file" path carried remoteImagesUnblocked over from whichever file was previously shown in that tab slot, silently unblocking images in a file the user never approved. Reset alongside the existing reveal-target reset. Covered by unit tests (gateRemoteImages, remoteImageBannerKind, the openPreviewTab recycle-reset, prefs.loadRemoteImages) and a live e2e pass driving the real app through all four flows. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Awesome, will test, merge and release tonight. What's the video recording software you're using? :D Thanks |
|
Thanks @adamatan, excellent work. This closes a real sandbox gap cleanly: the synchronous stash-and-strip in |
gate-remote-images-demo.mp4
Summary
This fixes a problem where markdown images were loaded from remote sources without the user's consent, which can trigger nasty stuff like tracking pixels and record the user's IP address. To fix it, we block images by default. The user can then either permit images just for that one file, or turn loading on globally.
fixes #69
The underlying issue: the webview sits outside the seatbelt + CONNECT proxy cage, so a
in a previewed markdown file fired an unprompted GET the moment the file was previewed — no click, no prompt — to whatever host untrusted content named (prompt injection, a dependency's README, a contributor's fork). Accepted as a known gap in bca4e13; this closes it.The gate.
gateRemoteImages()inMarkdownPreview.tsxintercepts everyhttp(s):<img>src before it ever reaches the DOM'ssrcattribute — stashed indata-md-remote-srcinstead, so the browser never issues the request while blocked. Gated onprefs.loadRemoteImages(new pref, default OFF) or a per-tabremoteImagesUnblockedoverride;data:/blob:sources and task-relative images (already handled byhydrateTaskImages) are untouched. The CSP itself is unchanged (still allowshttps:inimg-src) — this is a renderer gate, not a CSP tweak, since Tauri's CSP is one whole-webview policy that can't scope to a single component.UX. simion asked for "a settings toggle in general to auto load them, off by default... implementation should be as simple as possible" — landed on two escape hatches rather than adamatan's original three-tier "this time/always in this repo/always" proposal, skipping a persisted per-repo tier. A banner (reusing
TerminalExitedBanner, now with an opt-incenterlayout) surfaces both: "Show images" unblocks just the open document, session-only — dies with the tab, likemdView's per-tab override. "Always" flips the global pref; the same banner swaps its text/action in place (no toast) to a "Settings" link, which opens Settings, lands on General, and auto-scrolls to + briefly highlights the toggle (view.settingsHighlight, consumed and cleared on mount so a later manual visit doesn't re-trigger it).Bug caught during manual testing.
openPreviewTab's "recycle this preview tab into a different file" path (the standard single-click preview-tab reuse) carriedremoteImagesUnblockedover from whichever file was previously shown in that tab slot — unblocking a file the user never actually approved. Reset alongside the existing reveal-target reset, with a regression test.Test plan
npm test—gateRemoteImages(block/restore/leave-data-blob-alone/leave-task-relative-alone),remoteImageBannerKind(precedence: blocked always wins over a stale confirm), theopenPreviewTabrecycle-reset regression,prefs.loadRemoteImages(default + persistence)npx tsc -b/tmp/gate-test-repofixture covering raw-HTML-wrapped images (DOMPurify path),http:(non-TLS), mermaid + remote image together, many-images stress case, and negative controls (no images,data:only, a link that isn't an image)🤖 Generated with Claude Code