Webmail: block remote CSS fetches when remote images are off#222
Webmail: block remote CSS fetches when remote images are off#222ahmedhesham6 wants to merge 2 commits into
Conversation
With remote images off, only <img src> was rewritten. The sanitizer allows <style> and style attributes, so url(), @import, image-set() and img srcset all still fetched — enough for a sender to collect a read receipt, timestamp, and IP from a reader the UI reported as protected. blockRemoteContent() in sanitize.ts now covers every fetch surface in one pass: remote img src becomes the placeholder pixel, srcset with any remote candidate is dropped, and CSS loses url()/@import/ image-set() targets unless they are data: or cid:. CSS comments and escapes are normalized first so split or escaped spellings cannot smuggle a fetch past the patterns, and the comment/@import passes are linear scanners because the natural regexes backtrack quadratically on crafted input. Closes oblien#220
|
thank you for your support
CSS_IMAGE_SET = /…image-set([^;{}]*/ truncates its span at the first ;/{/} - but those are ordinary characters inside a CSS string token, so the browser parses the whole URL and fetches. Worst part: the realistic data: fallback + remote sibling hits this every time (the ; in ;base64 truncates the span before the remote candidate):
CSS_IMAGE_SET keys on the literal substring image-set(; image\2dset( doesn't match, but the browser resolves \2d→- while building the ident and forms a real image-set( function. (The url() form is safe here - CSS_REMOTE_URL is ident-independent - so only the bare-string form leaks.)
also Correctness (false positives / broken render)
suggested fix :
|
|
Thanks for the feedback, will update the gaps |
Bypasses (review on oblien#222): - image-set() spans are now found by a quote-aware balanced-paren scan instead of a regex ended at ;/{/} — those are ordinary characters inside a string token, so the regex span truncated before a remote candidate (data:…;base64 fallback + remote 2x leaked every time). - Escape decoding now includes \2d (-), so image\2dset( normalizes to image-set( and cannot dodge the ident match. - <img> attributes are walked sequentially instead of regexing src= out of the raw tag, so a decoy src= inside an alt/title value can no longer shield the real remote src. False positives: - Fragment-only url(#grad) is exempt — a same-document lookup, not a fetch. - stripImports skips string contents, so content:"@import" is no longer treated as an at-keyword. - image-set strings are rewritten by a sequential token scan; the regex re-anchored on the closing quote of an exempt string and mangled all-inline sets like image-set("data:a" 1x, "data:b" 2x).
|
Thanks for the sharp review — all six reproduce on the previous revision, and all six are fixed in faa955d. Bypasses:
False positives:
All previous behavior holds (34 checks including your five cases, the original issue repro, adversarial-input timing on the new scanners, and an end-to-end pass through the sanitizer). |
|
perfect, gonna check last time then merge, ty |
|
also are you on x ? would love to know your handle :" |
Summary
With remote images off,
processEmailContentrewrote only<img src>. The sanitizer allows<style>andstyleattributes, so CSSurl(),@import,image-set()— and<img srcset>— all still fetched. Any one of them gave a sender a read receipt, timestamp, and IP from a reader the UI reported as protected.This PR adds
blockRemoteContent()insanitize.tsas the single entry point for the preference and reduces the route to sanitize → block → return.What it blocks
<img src>— remote sources become the existing 1×1 placeholder pixel;data:/cid:sources are left alone so inline attachments keep rendering.<img srcset>— dropped whole when any candidate is remote, since srcset outranks src and would undo the rewrite. Candidates are parsed per the HTML rules (commas insidedata:URLs are not separators).url()— rewritten tonone, keeping the rest of the declaration valid;data:/cid:exempt.@import— dropped entirely, bothurl()and bare-string forms.image-set()— remote string arguments replaced with an inertdata:,, since they fetch with nourl(token.Covers both
<style>bodies andstyleattributes (with the entity encoding sanitize-html applies to attribute values handled correctly).Hardening details
url(/*x*/https://…)cannot split the token past the patterns.\75 rl(…)and@\69 mportcannot dodge the patterns. Other escapes are untouched.@importpasses are hand-written linear scanners: the natural regexes backtrack quadratically on crafted input (/*a/*a…, repeated unclosedurl(), which server-side is a denial-of-service lever.data:/cid:) backs every exemption so the passes cannot drift.(,@, or\, skip the work and pass through byte-identical.Blocking any of it sets the existing
hasBlockedImagesflag, so the current "remote content blocked" banner covers CSS too with no client change.Notes
Closes #220