Closed Bug 2018400 (CVE-2026-3846) Opened 5 months ago Closed 5 months ago

Cross-origin CSS rule disclosure and redirect URL leak via mOriginClean reset in StyleSheetInfo clone

Categories

(Core :: CSS Parsing and Computation, defect, P2)

defect

Tracking

()

VERIFIED FIXED
150 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr140 --- unaffected
firefox148 + verified
firefox149 + verified
firefox150 + verified

People

(Reporter: juny24602, Assigned: emilio)

References

(Regression)

Details

(5 keywords, Whiteboard: [client-bounty-form][adv-main148.0.2+])

Attachments

(4 files)

Attached file poc.html

Cross-origin CSS rule disclosure and redirect URL leak via mOriginClean reset in StyleSheetInfo clone

Summary

A cross-origin stylesheet's origin-clean flag can be reset by triggering
StyleSheetInfo's copy constructor, which does not copy the mOriginClean field.
When two <link> elements load the same cross-origin CSS URL, the
SharedStyleSheetCache deduplicates them into a shared StyleSheetInfo inner.
Modifying the sheet's MediaList (e.g. via sheet.media.appendMedium()) calls
WillDirty() -> EnsureUniqueInner(), which clones the shared inner. The clone's
mOriginClean defaults to true (the field initializer), bypassing the CSSOM
origin-clean check. This allows reading cross-origin CSS rules and, when the
stylesheet was loaded through a redirect, leaking the redirect destination URL
via @import insertion.

This is a variant of CVE-2025-31205 (WebKit cross-site CSS rule and redirect
URL disclosure, https://project-zero.issues.chromium.org/issues/408172161),
affecting Firefox through a different mechanism but with overlapping impact.

Root Cause

StyleSheetInfo (layout/style/StyleSheetInfo.h) declares mOriginClean with a
default member initializer of true:

bool mOriginClean = true;  // line 47

The copy constructor (layout/style/StyleSheet.cpp:349-367) copies mCORSMode,
mIntegrity, mSourceMapURL, and mContents, but does NOT copy mOriginClean:

StyleSheetInfo::StyleSheetInfo(StyleSheetInfo& aCopy, StyleSheet* aPrimarySheet)
    : mCORSMode(aCopy.mCORSMode),
      mIntegrity(aCopy.mIntegrity),
      mSourceMapURL(aCopy.mSourceMapURL),
      mContents(Servo_StyleSheet_Clone(...).Consume())
{
  AddSheet(aPrimarySheet);
}

Since mOriginClean is absent from the initializer list, it takes its default
value of true in the cloned inner, regardless of the original's value.

Attack Chain

  1. Load a cross-origin stylesheet via <link> (no CORS). The loader sets
    mOriginClean = false on the sheet's StyleSheetInfo inner (Loader.cpp:669).

  2. Load the same URL via a second <link>. SharedStyleSheetCache deduplicates:
    CreateSheet() finds a cache hit and returns Clone(), which shares the same
    StyleSheetInfo inner. The inner's mSheets now has length > 1, so
    HasUniqueInner() returns false.

  3. Modify the sheet's media attribute: sheet.media.appendMedium('screen').
    MediaList::DoMediaChange() (MediaList.cpp:50) calls mStyleSheet->WillDirty()
    with no origin check. WillDirty() calls EnsureUniqueInner()
    (StyleSheet.cpp:491). Since the inner is shared, CloneFor() creates a new
    StyleSheetInfo via the copy constructor -- mOriginClean is NOT copied and
    defaults to true.

  4. The sheet now has a unique inner with mOriginClean = true.
    AreRulesAvailable() (StyleSheet.cpp:928) passes, granting full CSSOM access:
    sheet.cssRules, insertRule(), deleteRule() all become available.

Suggested Fix

Copy mOriginClean in the StyleSheetInfo copy constructor:

    StyleSheetInfo::StyleSheetInfo(StyleSheetInfo& aCopy, StyleSheet* aPrimarySheet)
        : mCORSMode(aCopy.mCORSMode),
          mIntegrity(aCopy.mIntegrity),
+         mOriginClean(aCopy.mOriginClean),
          mSourceMapURL(aCopy.mSourceMapURL),
          mContents(Servo_StyleSheet_Clone(...).Consume())

Tested Version

Firefox 147.0.4 (release), source analysis on mozilla-central commit f8345be3cabc.

Flags: sec-bounty?
Duplicate of this bug: 2018401
Group: firefox-core-security → layout-core-security
Component: Security → CSS Parsing and Computation
Product: Firefox → Core

Gah, this is a dumb bug indeed, good find.

I'm not sure of the severity of this. Andrew do you know? Just in terms of holding off landing a test or what not.

Severity: -- → S3
Status: UNCONFIRMED → NEW
Ever confirmed: true
Flags: needinfo?(continuation)
Keywords: regression
Priority: -- → P3
Regressed by: 1995282
Attached file (secure)
Assignee: nobody → emilio
Status: NEW → ASSIGNED

It doesn't sound good. I guess a cross-origin data leak sounds like a sec-high to me. Dan, what do you think? This is a bit of an unusual issue so a second opinion is probably a good idea.

Flags: needinfo?(continuation) → needinfo?(dveditz)
Keywords: csectype-sop
Attached patch test.patchSplinter Review

(Sorry for the wrong format in previous comment, I don't know how to mark it as 'obsolete')

Attached a unittest (test.patch).

Before the (mOriginClean) patch:

Error Summary
-------------
layout/style/test/test_origin_clean_clone.html
  FAIL layout/style/test/test_origin_clean_clone.html - cssRules access must still be blocked after inner clone
    SimpleTest.ok@SimpleTest/SimpleTest.js:427:16
    link1.onload/link2.onload@layout/style/test/test_origin_clean_clone.html:38:7
    EventHandlerNonNull*link1.onload@layout/style/test/test_origin_clean_clone.html:28:3
    EventHandlerNonNull*@layout/style/test/test_origin_clean_clone.html:22:1
  FAIL layout/style/test/test_origin_clean_clone.html - Finished in 576ms

After patch:

Overall Summary
===============

mochitest-plain
~~~~~~~~~~~~~~~
Ran 3 checks (1 asserts, 1 subtests, 1 tests)
Expected results: 3
Unexpected results: 0
OK

Thanks! I have a test written locally which is pretty similar to that, but it's a web platform test (so, shared with other browsers and reusing a bit the existing test).

Attached file (secure)

Comment on attachment 9547044 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: not terribly hard, it's a one liner so relatively easy to figure out if you know browser internals.
  • Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
  • Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: beta, release
  • If not all supported branches, which bug introduced the flaw?: Bug 1995282
  • Do you have backports for the affected branches?: Yes
  • If not, how different, hard to create, and risky will they be?: Not hard, one liners.
  • How likely is this patch to cause regressions; how much testing does it need?: not very, one liner for well tested code-path
  • Is the patch ready to land after security approval is given?: Yes
  • Is Android affected?: Yes
Attachment #9547044 - Flags: sec-approval?

I'll mark it sec-high for now and Dan can adjust it as appropriate.

Keywords: sec-high

sec-high is standard for a web SOP violation. Even if the information is usually not that secret, in specific cases it might reveal information private to a logged-in user or similar.

Flags: needinfo?(dveditz)

Comment on attachment 9547044 [details]
(secure)

Beta/Release Uplift Approval Request

  • User impact if declined/Reason for urgency: SOP bypass
  • Is this code covered by automated tests?: Yes
  • Has the fix been verified in Nightly?: No
  • Needs manual test from QE?: Yes
  • If yes, steps to reproduce: comment 0
  • List of other uplifts needed: none
  • Risk to taking this patch: Low
  • Why is the change risky/not risky? (and alternatives if risky): One-liner.
  • String changes made/needed: none
  • Is Android affected?: Yes
Attachment #9547044 - Flags: approval-mozilla-release?
Attachment #9547044 - Flags: approval-mozilla-beta?
Flags: qe-verify+

The severity field for this bug is set to S3. However, the bug is flagged with the sec-high keyword.
:emilio, could you consider increasing the severity of this security bug?

For more information, please visit BugBot documentation.

Flags: needinfo?(emilio)

Sure.

Severity: S3 → S2
Flags: needinfo?(emilio)

Because this doesn't affect ESR it is a good candidate for the mid-cycle 148 point release if release managers approve it.

Comment on attachment 9547044 [details]
(secure)

Please remove the mention of cloning from the commit message; given the simplicity of the patch maybe just mention the bug number and r=. We don't want to give a head start to people who don't already know browser internals. Please don't land the test until after we release the fix. If we can get it into a 148 point-release then after April 1 is good, but if this doesn't ship until 149 then wait until May. I'll set the reminder for the later date but feel free to modify it if we do uplift sooner.

sec-approval+ to land in central and request uplifts after you redact the commit message

Attachment #9547044 - Flags: sec-approval? → sec-approval+
Whiteboard: [client-bounty-form] → [client-bounty-form][reminder-test 2026-04-29]

The bug is marked as tracked for firefox148 (release), tracked for firefox149 (beta) and tracked for firefox150 (nightly). However, the bug still has low priority.

:fgriffith, could you please increase the priority for this tracked bug? Given that it is a regression and we know the cause, we could also simply backout the regressor. If you disagree with the tracking decision, please talk with the release managers.

For more information, please visit BugBot documentation.

Flags: needinfo?(fgriffith)
Flags: needinfo?(fgriffith)
Priority: P3 → P2
Group: layout-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 5 months ago
Resolution: --- → FIXED
Target Milestone: --- → 150 Branch
QA Whiteboard: [uplift][qa-ver-needed-c150/b149]
Attachment #9547044 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [uplift][qa-ver-needed-c150/b149] → [sec] [uplift] [qa-ver-needed-c150/b149]

Verified as fixed on the latest Firefox for Android Nigthly 150.0a1 from 3/3, and on Firefox for Android Beta 149.0b3 with a Pixel 9 Pro (Android 16).
Tapping on the link from Comment 0, on "Disclose rules" or "Disclose redirect URL" are not providing any results (the buttons present different behaviors with and without the patch applied).

Flags: qe-verify+

Reproduced the leak on Firefox 148 by clicking Disclose rules and Disclose redirect URL on Windows 11.

Verified as fixed using the latest Nightly 150.0a1 and Firefox 149.0b3 on Windows 11, Ubuntu 24.04, and macOS 15 — no leak occurs when clicking the Disclose buttons.

@emilio - is there anything else we should manually verify?

Flags: needinfo?(emilio)

No, that's great, ty!

Flags: needinfo?(emilio)
Flags: sec-bounty? → sec-bounty+

Comment on attachment 9547044 [details]
(secure)

Approved for 148.0.2

Attachment #9547044 - Flags: approval-mozilla-release? → approval-mozilla-release+

Tom, would this be similar to your work for LoadInfo?

Whiteboard: [client-bounty-form][reminder-test 2026-04-29] → [client-bounty-form][reminder-test 2026-04-29][adv-main148.0.2+]
Alias: CVE-2026-3846

Verified as fixed using Firefox 148.0.8 - tested on Windows 11, Ubuntu 24.04 and macOS 15 - no leak occurs when clicking the Disclose buttons.

Status: RESOLVED → VERIFIED
QA Whiteboard: [sec] [uplift] [qa-ver-needed-c150/b149] → [sec] [uplift] [qa-ver-done-c150/b149]

2 months ago, dveditz placed a reminder on the bug using the whiteboard tag [reminder-test 2026-04-29] .

emilio, please refer to the original comment to better understand the reason for the reminder.

Flags: needinfo?(emilio)
Whiteboard: [client-bounty-form][reminder-test 2026-04-29][adv-main148.0.2+] → [client-bounty-form][adv-main148.0.2+]

queued test for landing :)

Flags: needinfo?(emilio)
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: