Closed Bug 1906744 (CVE-2024-8382) Opened 1 year ago Closed 1 year ago

ChromeOnly event interfaces become exposed to web content when chromeEventHandler listener callbacks run

Categories

(Core :: DOM: Core & HTML, defect)

Firefox 130
x86_64
Linux
defect

Tracking

()

VERIFIED FIXED
131 Branch
Tracking Status
firefox-esr115 130+ verified
firefox-esr128 130+ verified
firefox129 --- wontfix
firefox130 + verified
firefox131 + verified

People

(Reporter: gregp, Assigned: peterv)

Details

(Keywords: csectype-priv-escalation, reporter-external, sec-moderate, Whiteboard: [adv-main130+][adv-esr128.2+][adv-esr115.15+])

Attachments

(6 files)

Steps to reproduce:

  1. Navigate to https://www.google.com/
  2. Open DevTools console
  3. Run typeof NotifyPaintEvent

Actual results:
type is function

Expected results:
type is undefined

This is surprising given that this interface has been ChromeOnly since bug 1334957

In a local build, commenting out this MozAfterPaint listener in AboutReaderChild results in NotifyPaintEvent not being present in web content
https://searchfox.org/mozilla-central/rev/6936c4c3fc9bee166912fce10104fbe0417d77d3/browser/actors/AboutReaderChild.sys.mjs#199-202

Another example is StyleSheetApplicableStateChangeEvent. This one is fun because StyleSheetApplicableStateChanged events only fire when DevTools are open
https://searchfox.org/mozilla-central/rev/a5bcdc6afbcfccf9f293ace0fddf11307e5bc22c/devtools/server/actors/utils/stylesheets-manager.js#252-256

So, it's possible to construct a test case that detects when DevTools are opened by looking for the existence of this interface.

Steps to reproduce:

  1. Navigate to the attached test case
  2. Open DevTools

Actual results:
Test case says devtools opened

Expected results:
Test case doesn't say that

Olli, I wonder if there's fundamental problem here, this looks surprising

Severity: -- → S3
Flags: needinfo?(smaug)

I can't quite tell from comment 0 - does this only work from the devtools console?

Flags: needinfo?(gregp)
Group: dom-core-security

(In reply to :Gijs (he/him) from comment #2)

I can't quite tell from comment 0 - does this only work from the devtools console?

To be clear - from comment 0:

(In reply to Gregory Pappas [:gregp] from comment #0)

Steps to reproduce:

  1. Navigate to https://www.google.com/
  2. Open DevTools console
  3. Run typeof NotifyPaintEvent

Does executing the typeof from outside devtools (ie directly from web content) produce undefined or function, if run after the AboutReader MozAfterPaint stuff has run?

Does executing the typeof from outside devtools (ie directly from web content) produce undefined or function, if run after the AboutReader MozAfterPaint stuff has run?

function. Test case: data:text/html,<script>requestIdleCallback(()=>requestIdleCallback(()=>alert(typeof NotifyPaintEvent)))</script>

Flags: needinfo?(gregp)

This broke at some point, on Jan 1 2021 this returned undefined.

Keywords: regression

mozregression says this regressed with https://hg.mozilla.org/mozilla-central/pushloghtml?fromchange=a8d37f94a6364103b7f373b9938ce7a62d9db9e6&tochange=5b0962239a45c0cdae5d495e8ee3786a7c5a07f3 though I wonder if the requestIdleCallback based testcase is racy.

Also I don't see anything obvious in that pushlog. :-(

Providing web content access to ChromeOnly interfaces is worse than S3.

In some further testing I'm struggling to consistently reproduce things - is the trick here that things are only exposed once some chrome actor accesses the thing in question, or something? (that is, I'm trying to use e.g. typeof StyleSheetApplicableStateChangeEvent and that still produces undefined in most cases, but the attached testcase does notice when I open devtools. How does that work, exactly? Debugging this at this time of night is not really working well for me...)

Severity: S3 → --

Peter might also have some ideas.

Flags: needinfo?(peterv)

(In reply to :Gijs (he/him) from comment #6)

mozregression says this regressed with https://hg.mozilla.org/mozilla-central/pushloghtml?fromchange=a8d37f94a6364103b7f373b9938ce7a62d9db9e6&tochange=5b0962239a45c0cdae5d495e8ee3786a7c5a07f3 though I wonder if the requestIdleCallback based testcase is racy.

Also I don't see anything obvious in that pushlog. :-(

The data uri test case is very racy and is not useful for bisecting

In some further testing I'm struggling to consistently reproduce things - is the trick here that things are only exposed once some chrome actor accesses the thing in question, or something?

In testing locally, using something like function(){} as the event listener callback still results in the interface being exposed to web content. Removing the addEventListener call results in the interface not being exposed to web content.

that is, I'm trying to use e.g. typeof StyleSheetApplicableStateChangeEvent and that still produces undefined in most cases, but the attached testcase does notice when I open devtools. How does that work, exactly?

So, the test case constructs a stylesheet and checks "StyleSheetApplicableStateChangeEvent" in window. This ends up being a reliable way to detect that DevTools has been opened.

FWIW I can reproduce this as far as back as Nightly 42.0a1 (2015-07-23). Anything older instantly crashes on my machine.

STR for Nightly 42.0a1 (2015-07-23):

  1. Launch the ancient Nightly:
    mozregression --launch '2015-07-23' --preferences 'prefs.json'
    // prefs.json
    {
      "browser.tabs.remote.autostart.2": false,
      "devtools.debugger.remote-enabled": true,
      "devtools.chrome.enabled": true
    }
    
  2. Navigate to https://www.google.com/
  3. Open DevTools console
  4. Run typeof StyleSheetApplicableStateChangeEvent
  5. Confirm that it is undefined
  6. Open browser toolbox console
  7. Run
    gBrowser.selectedBrowser.contentDocument.styleSheetChangeEventsEnabled = true;
    gBrowser.selectedBrowser.docShell.chromeEventHandler.addEventListener("StyleSheetApplicableStateChanged", function(){});
    
  8. Go back to DevTools console
  9. Run typeof StyleSheetApplicableStateChangeEvent
  10. Confirm it is still undefined
  11. Run Array.from(document.styleSheets).forEach(sheet => sheet.disabled = true)
  12. Run typeof StyleSheetApplicableStateChangeEvent

Actual results:
Type is function

Expected results:
Type is undefined

It looks like DevTools started using this event in 2017 (bug 1224558), so a test case like attachment 9411638 [details] would not work in these very old builds.

Providing web content access to ChromeOnly interfaces is worse than S3.

can the web page use those interfaces, or can it only see that they exist? That is, is the ChromeOnly attribute still enforced even though the object itself is weirdly exposed?

can the web page use those interfaces?

Yes. For example, new StyleSheetApplicableStateChangeEvent({}) does work in web content.

Web page can create such event, but it will be an untrusted event.

Flags: needinfo?(smaug)
Assignee: nobody → peterv
Status: NEW → ASSIGNED
Flags: needinfo?(peterv)

Based on comment #6, this bug contains a bisection range found by mozregression. However, the Regressed by field is still not filled.

:peterv, if possible, could you fill the Regressed by field and investigate this regression?

For more information, please visit BugBot documentation.

Flags: needinfo?(peterv)

I don't think this is really a regression, the WebIDL bindings have always worked like this and I don't remember that pre-WebIDL bindings had a mechanism for chrome-only interfaces (pretty sure we just exposed all nsIDOM* interfaces).

(In reply to :Gijs (he/him) from comment #6)

In some further testing I'm struggling to consistently reproduce things - is the trick here that things are only exposed once some chrome actor accesses the thing in question, or something?

The specific conditions are that chrome code creates a WebIDL object in the web content global's scope but for a WebIDL interface that's marked [ChromeOnly]. For example chrome code doing webContentWindow.createAChromeOnlyFoo(), or when we're calling an event handler in chrome code for an event created in the web content global and fired on an object from the web content global's scope. That will wrap the WebIDL object from the web content scope in an Xray before passing it to the chrome code. If at that point the chrome code has not accessed the WebIDL interface object for that interface (eg by enumerating the properties on the web content global or something), then we'll define a property on the web content global for that [ChromeOnly] interface. We do have to create the WebIDL interface object, so that we can wrap it in an Xray too (and put it on the prototype chain for the Xray for the WebIDL object), but we might be able to skip defining the property.

Flags: needinfo?(peterv)

(In reply to Daniel Veditz [:dveditz] from comment #10)

can the web page use those interfaces, or can it only see that they exist? That is, is the ChromeOnly attribute still enforced even though the object itself is weirdly exposed?

It can use the interface objects to create objects (new Foo()), the objects will be created in the web content scope. It can also access any static WebIDL properties on them (Foo.staticWebIDLMethod()).

Attachment #9412971 - Attachment description: WIP: Bug 1906744 - ChromeOnly event interfaces become exposed to web content when chromeEventHandler listener callbacks run. → WIP: Bug 1906744 - Check if constructor is enabled before installing named property.

This is only a sec-moderate because it does require devtools doing something, but it feels like there's potentially a lot of surface area exposed here, and the problem seems to be understood, so I'm going to mark this S2.

Severity: -- → S2

Found another one

ChromeOnly CaretStateChangedEvent becomes exposed to web content when the user selects text because of this mozcaretstatechanged listener
https://searchfox.org/mozilla-central/rev/8c6edfe25c094e032a27722ef30f69555f556bf8/mobile/shared/chrome/geckoview/geckoview.js#672-676

(Happens only on Android; Desktop doesn't have any mozcaretstatechange listeners)

Attachment #9412971 - Attachment description: WIP: Bug 1906744 - Check if constructor is enabled before installing named property. → Bug 1906744 - Check if constructor is enabled before installing named property. r?mccr8!
Attachment #9412972 - Attachment description: WIP: Bug 1906744 - ChromeOnly event interfaces become exposed to web content when chromeEventHandler listener callbacks run - testcase. → Bug 1906744 - ChromeOnly event interfaces become exposed to web content when chromeEventHandler listener callbacks run - testcase. r?mccr8!
Pushed by pvanderbeken@mozilla.com: https://hg.mozilla.org/integration/autoland/rev/2fa58daa8086 Check if constructor is enabled before installing named property. r=mccr8,dom-storage-reviewers,janv,asuth

Backed out for failing browser_aboutNetError.js:
https://hg.mozilla.org/integration/autoland/rev/5184e66f8e3be21678ce937d2be3bdae848e5a7c

Push with failures
Failure log

[task 2024-07-26T15:54:09.881Z] 15:54:09     INFO - Entering test bound resetToDefaultConfig
[task 2024-07-26T15:54:09.881Z] 15:54:09     INFO - Change TLS config to cause page load to fail, check that reset button is shown and that it works
[task 2024-07-26T15:54:09.888Z] 15:54:09     INFO - Loading and waiting for the net error
[task 2024-07-26T15:54:09.889Z] 15:54:09     INFO - Buffered messages logged at 15:52:40
[task 2024-07-26T15:54:09.889Z] 15:54:09     INFO - Console message: [JavaScript Error: "ReferenceError: Localization is not defined" {file: "chrome://global/content/aboutNetError.mjs" line: 856}]
[task 2024-07-26T15:54:09.891Z] 15:54:09     INFO - setNetErrorMessageFromCode@chrome://global/content/aboutNetError.mjs:856:20
[task 2024-07-26T15:54:09.892Z] 15:54:09     INFO - initPage@chrome://global/content/aboutNetError.mjs:625:3
[task 2024-07-26T15:54:09.892Z] 15:54:09     INFO - @chrome://global/content/aboutNetError.mjs:1567:1
[task 2024-07-26T15:54:09.893Z] 15:54:09     INFO - 
[task 2024-07-26T15:54:09.893Z] 15:54:09     INFO - Buffered messages finished
[task 2024-07-26T15:54:09.894Z] 15:54:09     INFO - TEST-UNEXPECTED-FAIL | browser/base/content/test/about/browser_aboutNetError.js | Test timed out - 
Flags: needinfo?(peterv)

Second failure log

TEST-UNEXPECTED-FAIL | browser/base/content/test/siteIdentity/browser_check_identity_state.js | Test timed out -

We have code that creates a Localization object in a global where it shouldn't be exposed, by relying on the bug that I'm trying to fix: https://searchfox.org/mozilla-central/rev/dd421ae14997e3bebb9c08634633a4a3e3edeffc/toolkit/content/aboutNetError.mjs#856

Looking at the l10n interfaces and properties there is some weirdness. document.l10n is marked as [Func="Document::DocumentSupportsL10n"]. Its interface is DocumentL10n, which is not exposed because it's marked [LegacyNoInterfaceObject] and it doesn't have a constructor, so it's really only accessible if Document::DocumentSupportsL10n is true. The interfaces on its prototype chain are DOMLocalization and Localization, these are both marked [Func="IsChromeOrUAWidget"]. [Func="IsChromeOrUAWidget"] is different than [Func="Document::DocumentSupportsL10n"], but if the intention is to avoid exposing DOMLocalization and Localization to globals that return true for IsChromeOrUAWidget but false for Document::DocumentSupportsL10n then that fails because of the prototype chain (Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Object.getPrototypeOf(document.l10n)), "constructor").value returns the interface object for DOMLocalization). What is the intention for these interfaces? Is marking DOMLocalization and Localization as [Func="Document::DocumentSupportsL10n"] the right fix, or do we need to try to restrict access to them? I don't know in detail what these expose, so hard to judge for me.

Flags: needinfo?(zibi)
Flags: needinfo?(peterv)
Flags: needinfo?(earo)

(In reply to Peter Van der Beken [:peterv] from comment #24)

[...] The interfaces on its prototype chain are DOMLocalization and Localization, these are both marked [Func="IsChromeOrUAWidget"]. [Func="IsChromeOrUAWidget"] is different than [Func="Document::DocumentSupportsL10n"], but if the intention is to avoid exposing DOMLocalization and Localization to globals that return true for IsChromeOrUAWidget but false for Document::DocumentSupportsL10n then that fails because [...] What is the intention for these interfaces?

These interfaces and their restrictions are from before my time, and I'm not completely sure why they're different. However, I'm not aware of any situation where IsChromeOrUAWidget is false while Document::DocumentSupportsL10n is true. In practice, I would be surprised by being able to use document.l10n but not Localization or DOMLocalization from the same code.

Is marking DOMLocalization and Localization as [Func="Document::DocumentSupportsL10n"] the right fix, or do we need to try to restrict access to them? I don't know in detail what these expose, so hard to judge for me.

If that works with our current usage, then it should be fine. For instance, in the aboutNetError code, the Localization is used directly instead of document.l10n in order to format messages synchronously, which the latter does not allow.

Flags: needinfo?(earo)

I would be surprised by being able to use document.l10n but not Localization or DOMLocalization from the same code.

I don't think this would be even possible, nor should it. document.l10n exposes DocumentL10n which is an implementation of DOMLocalization [0].

For instance, in the aboutNetError code, the Localization is used directly instead of document.l10n in order to format messages synchronously, which the latter does not allow.

That's just about sync vs async. Localization is a dual interface (sync+async), DOMLocalization and DocumentL10n are async only. The intention behind this decision was to move everything to async, but we had to leave a sync-escape-hatch for cases like the one you just described. (and at the time of Fluent introduction we had WAY more of since Properties API is sync).
The intention behind it was to move away from sync over time as we migrate Properties to use DOMLocalization, l10n-id etc.

What is the intention for these interfaces?

I'm a bit fuzzy on the details here, but IIRC the intention here is to keep document.l10n+DocumentL10n+DOMLocalization+Localization bound to the same surface area. What the area is may change. We started with Chrome, but we talked about exposing it to extensions and we have a bunch of insecure documents that we have to localize - about* documents are such an example. I think we closely followed what DTD was exposing as we target replacing that.
It may very well be that DTD was exposing too much and the bounds were the subject of this bug as well.

The question is then, how can we expose localization API to about* documents without exposing it to the Web Content.

[0] https://searchfox.org/mozilla-central/source/dom/l10n/DocumentL10n.h#44

Flags: needinfo?(zibi)

(In reply to Eemeli Aro [:eemeli] from comment #25)

However, I'm not aware of any situation where IsChromeOrUAWidget is false while Document::DocumentSupportsL10n is true.

about:* error pages are one example.

Drat, sorry, I got lost in the booleans and meant to say the opposite: I'm not aware of any situation where IsChromeOrUAWidget is true while Document::DocumentSupportsL10n is false.

(In reply to Zibi Braniecki [:zbraniecki][:gandalf] from comment #26)

What is the intention for these interfaces?

I'm a bit fuzzy on the details here, but IIRC the intention here is to keep document.l10n+DocumentL10n+DOMLocalization+Localization bound to the same surface area. What the area is may change. We started with Chrome, but we talked about exposing it to extensions and we have a bunch of insecure documents that we have to localize - about* documents are such an example. I think we closely followed what DTD was exposing as we target replacing that.

To be clear, loading a DTD and exposing a JS api is vastly different in terms of the security-sensitive surface. I'll expose DOMLocalization and Localization when Document::DocumentSupportsL10n is true. I'm assuming that you all have determined that that's safe, since you've put them on the prototype chain of DocumentL10n.

I'm assuming that you all have determined that that's safe, since you've put them on the prototype chain of DocumentL10n

We've done security review when we landed the API and it has not changed since the landing (yay!).

I'll expose DOMLocalization and Localization when Document::DocumentSupportsL10n is true.

It may be prudent to verify that the heuristics of DocumentSupportsL10n are sane. This method has moved quite a bit and Gecko's principal model evolved since I wrote all of this.

I reported that L10N is exposed to all extensions around 4 years ago in bug 1696755. I haven't tested if this is still the case.

Pushed by pvanderbeken@mozilla.com: https://hg.mozilla.org/integration/autoland/rev/e9975f346b2b Check if constructor is enabled before installing named property. r=mccr8,dom-storage-reviewers,janv,asuth,eemeli
Backout by imoraru@mozilla.com: https://hg.mozilla.org/integration/autoland/rev/1dfd1678c928 Backed out changeset e9975f346b2b for causing multiple failures. CLOSED TREE
Flags: needinfo?(gregp) → needinfo?(peterv)
Pushed by pvanderbeken@mozilla.com: https://hg.mozilla.org/integration/autoland/rev/393ab27c060b Check if constructor is enabled before installing named property. r=mccr8,dom-storage-reviewers,janv,asuth,eemeli
Backout by nbeleuzu@mozilla.com: https://hg.mozilla.org/integration/autoland/rev/520eea124117 Backed out changeset 393ab27c060b for Hazard bustages on Localization.cpp

Backed for hazard failure in intl/l10n/Localization.cpp
https://hg.mozilla.org/integration/autoland/rev/520eea124117a4edac931d5d099b28902778737f

Push with failures
Failure log

[task 2024-08-06T21:53:29.391Z] TEST-UNEXPECTED-FAIL | hazards | unrooted 'aObject' of type 'JSObject*' live across GC call at intl/l10n/Localization.cpp:155
[task 2024-08-06T21:53:29.391Z] TEST-UNEXPECTED-FAIL | hazards | 1 rooting hazards detected

Pushed by pvanderbeken@mozilla.com: https://hg.mozilla.org/integration/autoland/rev/2c1f0772de87 Check if constructor is enabled before installing named property. r=mccr8,dom-storage-reviewers,janv,asuth,eemeli
Group: dom-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 1 year ago
Resolution: --- → FIXED
Target Milestone: --- → 131 Branch
Flags: sec-bounty?
Flags: needinfo?(peterv)

Please nominate this for Beta, ESR128, and ESR115 approval. It grafts cleanly to Beta and ESR128. It'll need some rebasing for ESR115, however.

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

gregp, this is a truly great find. We're currently seeing this as a sec-moderate and will award it as such. but we were wondering if there's a way to do something of a higher severity with this kind of bug.
If you want, you can continue working on this and see if there's a high severity security bug lurking underneath. At that point, we would happily re-evaluate our bug bounty. Thank you.

Comment on attachment 9412971 [details]
Bug 1906744 - Check if constructor is enabled before installing named property. r?mccr8!

Beta/Release Uplift Approval Request

  • User impact if declined: Allows access from web content to DOM objects that should only be exposed to chrome code. We're unsure that this is exploitable.
  • Is this code covered by automated tests?: Yes
  • Has the fix been verified in Nightly?: Yes
  • Needs manual test from QE?: Yes
  • If yes, steps to reproduce: Automated testcase in https://phabricator.services.mozilla.com/D216672 hasn't landed yet, but steps from comment 0 could be used.
  • List of other uplifts needed: None
  • Risk to taking this patch: Medium
  • Why is the change risky/not risky? (and alternatives if risky): There might be subtle breakage when derived WebIDL interfaces are inconsistent in their exposure wrt their parent interfaces. We caught one of these through tests. It does seem unlikely that there are more.
  • String changes made/needed:
  • Is Android affected?: Yes
Flags: needinfo?(peterv)
Attachment #9412971 - Flags: approval-mozilla-beta?
Flags: qe-verify+

Comment on attachment 9412971 [details]
Bug 1906744 - Check if constructor is enabled before installing named property. r?mccr8!

ESR Uplift Approval Request

  • If this is not a sec:{high,crit} bug, please state case for ESR consideration: We're unsure that this is exploitable, but if it is it could raise the security rating.
  • User impact if declined: Allows access from web content to DOM objects that should only be exposed to chrome code.
  • Fix Landed on Version: 131
  • Risk to taking this patch: Medium
  • Why is the change risky/not risky? (and alternatives if risky): There might be subtle breakage when derived WebIDL interfaces are inconsistent in their exposure wrt their parent interfaces. We caught one of these through tests. It does seem unlikely that there are more.
Attachment #9412971 - Flags: approval-mozilla-esr128?
Attachment #9412971 - Flags: approval-mozilla-esr115?

Comment on attachment 9412971 [details]
Bug 1906744 - Check if constructor is enabled before installing named property. r?mccr8!

Approved for 130.0b9.

Attachment #9412971 - Flags: approval-mozilla-beta? → approval-mozilla-beta+

A patch has been attached on this bug, which was already closed. Filing a separate bug will ensure better tracking. If this was not by mistake and further action is needed, please alert the appropriate party. (Or: if the patch doesn't change behavior -- e.g. landing a test case, or fixing a typo -- then feel free to disregard this message)

Attachment #9420494 - Attachment description: WIP: Bug 1906744 - Check if constructor is enabled before installing named property. ESR115 port. → Bug 1906744 - Check if constructor is enabled before installing named property. ESR115 port.

Comment on attachment 9412971 [details]
Bug 1906744 - Check if constructor is enabled before installing named property. r?mccr8!

Approved for 128.2esr and 115.15esr.

Attachment #9412971 - Flags: approval-mozilla-esr128?
Attachment #9412971 - Flags: approval-mozilla-esr128+
Attachment #9412971 - Flags: approval-mozilla-esr115?
Attachment #9420494 - Flags: approval-mozilla-esr115+
QA Whiteboard: [post-critsmash-triage]

Reproduced the initial issue described in comment 0 using an old Nightly build from 2024-07-08. Verified that using latest Nightly build 131.0a1, Firefox 130.0RC, Firefox 128.2.0esr and Firefox 115.15.0esr the issues is not reproducible anymore across platforms. (Windows 11, macOS 13.6 and Ubuntu 22.04)

Whiteboard: [adv-main130+][adv-esr128.2+][adv-esr115.15+]
Alias: CVE-2024-8382
Group: core-security-release
Pushed by gp3033@protonmail.com: https://hg.mozilla.org/integration/autoland/rev/0ad92ceabe10 ChromeOnly event interfaces become exposed to web content when chromeEventHandler listener callbacks run - testcase. r=mccr8
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: