Closed Bug 1538008 (CVE-2019-9812) Opened 5 years ago Closed 4 years ago

[ ZDI-CAN-8375] UXSS priv-esc via sync (install arbitrary extensions & set arbitrary preferences)

Categories

(Firefox :: Sync, defect, P1)

defect

Tracking

()

RESOLVED FIXED
Tracking Status
firefox-esr60 69+ verified
firefox-esr68 69+ verified
firefox66 + wontfix
firefox67 + wontfix
firefox68 + verified
firefox69 + verified

People

(Reporter: Alex_Gaynor, Assigned: tjr)

References

Details

(Keywords: csectype-priv-escalation, csectype-sandbox-escape, sec-high, Whiteboard: [adv-main69+][adv-esr68.1+][adv-esr60.9+][do not publish until Bug 1538015 is shipped.])

UXSS -> privesc via sync (install arbitrary extensions & set arbitrary preferences)

RCE in the Firefox renderer can be turned into UXSS via renderer patches (see
my Pwn2Own exploit).

Now login in to sync via UXSS on
https://accounts.firefox.com/signin?service=sync&context=fx_desktop_v3&entrypoint=menupanel
(this can probably be achieved via XPCOM but this is the easiest way to
do it). This lets you sync the victim browser with an attacker-controlled
account, leading immediately to:

  1. Privesc to web extension
  2. Arbitrary preference write by setting services.sync.prefs.sync.<pref> = true
    • On Linux: security.sandbox.content.level = 0 -> Boom, no sandbox!
      (on Windows + macOS we are capped at 1, but we can at least bypass the
      desktop & job lockdown)
    • browser.tabs.remote.autostart -> turning this off disables e10s, but only after a restart
    • browser.download.{dir,useDownloadDir} -> probably arbitrary file write, have not tested this
    • network.proxy.autoconfig_url = <path to PAC file>, network.proxy.type = 2,
      This script will be evaluated in browser process, so we can chain with a
      Spidermonkey exploit (such as the one I used at Pwn2Own)

I'm sure there is other interesting settings which lead to a full sandbox escape.

Summary: ZDI-CAN-8375 → [ ZDI-CAN-8375] UXSS priv-esc via sync (install arbitrary extensions & set arbitrary preferences)
Alias: CVE-2019-9812
No longer blocks: CVE-2019-11732
Severity: normal → blocker
Priority: -- → P1
Component: General → Sync
Product: Core → Firefox

Arbitrary preference write by setting services.sync.prefs.sync.<pref> = true

Noting this because it wasn't obvious to me from reading the STR: when Firefox syncs prefs, it also syncs the list of prefs to sync, which is why this can be used to write to arbitrary prefs.

Group: core-security → firefox-core-security

Ugh. I've been trying to think of any short-term mitigations we could apply here, but haven't come up with much of anything; this is basically the different parts of Sync all working as designed.

For the "install a web extension via sync" case, the addons sync engine already has some basic checks to protect against installing things from disreputable sources, see e.g.:

https://dxr.mozilla.org/mozilla-central/rev/b2f1edb41241d3da6ded54edf38cca1d2d08325b/services/sync/modules/engines/addons.js#619

They wouldn't protect against installing a webextension that's available properly from AMO though. And from a glance at the code, these checks may only be applied when writing data to sync, not when pulling down data from sync, so I don't know if they would help in this case where the sync server has been filled with malicious data.

We probably have a followup here to ensure those checks apply on read instead of on write, but it's not obvious there would be any more checks we could do to prevent syncing down "bad" webextensions.

For the "arbitrary preference write via sync" case, I guess we could try to allowlist/blocklist certain classes of pref, but I'd expect a long tail of either discovering new prefs that aren't safe (if we try to blocklist) or getting bugs from users whose custom workflow we broke (if we try to allowlist). :pauljt filed Bug 1538015 to dig through this in more detail.

The underlying problem here is that https://accounts.firefox.com has the special ability to silently sign the browser in to sync, by sending a WebChannel message of type "login". We should find a way to stop doing that, and I filed Bug 1538024 to investigate.

I don't know enough about the internals of the browser to know whether there are things we could do prevent that from happening in "suspicious" cases without breaking the various sync onboarding flows that rely on it. For example, I wonder if we have enough information when dispatching the WebChannel message here:

https://dxr.mozilla.org/mozilla-central/rev/b2f1edb41241d3da6ded54edf38cca1d2d08325b/toolkit/modules/WebChannel.jsm#66

To detect that it's coming from an iframe or other unexpected source, rather than from a top-level tab opened to accounts.firefox.com. I also don't know whether that would have helped in this case.

I wonder if we have enough information when dispatching the WebChannel message
here [...] to detect that it's coming from an iframe or other unexpected source

:pauljt talked through this with me a bit, and it doesn't sound like there's much we could do here; if the child process is sufficiently compromised then it can make itself indistinguishable from a "good" login page.

I think the right path forward on the FxA front is Bug 1538024, removing the ability for web content to sign the browser in to sync.

Shouldn't opening https://accounts.firefox.com always to a new process help?

Shouldn't opening https://accounts.firefox.com always to a new process help?

Yes, I believe it would, basically do "site isolation" for accounts.firefox.com (and maybe for other privileged mozilla domains).

Well, we should be able to do that, similarly as what we do for file:// urls (at least top level page loads).

I guess we'd need to simply block iframing accounts.firefox.com for the time being? Would that break anything?

This was a bonus vulnerability from Niklas Baumstark at pwn2own (not actually used in any entries though).

(In reply to Alex Gaynor [:Alex_Gaynor] from comment #7)

I guess we'd need to simply block iframing accounts.firefox.com for the time being? Would that break anything?

I'm confused, what does this have to do with iframing? Any load of accounts.firefox.com in a compromised content process would be sufficient for the described attack, no?

Right, it's not iframing specific -- I just meant there's always two pieces: 1) loading the origin in it's own process, 2) not loading it in any other process :-)

I guess we'd need to simply block iframing accounts.firefox.com for the time being? Would that break anything?

We used to allow certain trusted origins to iframe accounts.firefox.com (via some awful code to generate X-Frame-Options and friends on-the-fly) but I believe all those integrations have been removed. I can't think of any legitimate reason for a site to be loading accounts.firefox.com in an iframe. Shane?

Flags: needinfo?(stomlinson)

(In reply to Ryan Kelly [:rfkelly] from comment #11)

I guess we'd need to simply block iframing accounts.firefox.com for the time being? Would that break anything?

We used to allow certain trusted origins to iframe accounts.firefox.com (via some awful code to generate X-Frame-Options and friends on-the-fly) but I believe all those integrations have been removed. I can't think of any legitimate reason for a site to be loading accounts.firefox.com in an iframe. Shane?

Note that I expect X-FRAME-OPTIONS/CSP will not be relevant here. In a compromised child process, the attacker can subvert any security checks internal to that process, which I believe these are. (correct me if Im wrong, but I think they hang off the document object in the child process?).

(In reply to Alex Gaynor [:Alex_Gaynor] from comment #10)

Right, it's not iframing specific -- I just meant there's always two pieces: 1) loading the origin in it's own process, 2) not loading it in any other process :-)

Actually I think there might be THREE things here (cue spanish inquistion intro) :

  1. loading the origin in it's own process
  2. Not loading the the origin in another process
  3. Not loading ANY other web content in the Firefox accounts process

Because we have to make sure not to load untrusted content into our trusted process.

And then FOUR (amongst our diverse weaponary...), make sure that either our IPC model ensures that FxA privileges are ONLY granted to this blessed process. That is either done by:

  • ensuring that the parent process will only listen for FxA web channel message FROM our blessed process
  • adding a check in the FxA which checks the message.target to ensure it corresponds to the correct process (im not sure if this is possible though with current messaging architecture, maybe it depends on Fission pieces).

My understanding is that these privileges are provided by FxAccountsWebChannel.jsm ( and the mobile version here). Assuming we force isolation of accounts.firefox.com in a seperate process, we need to ensure that the parent process will only act upon FxA messages from this process.

WebChannel architecture may provides this already though? I'm not sure if the approprate listeners are only created for a process that is loading accounts.firefox.com, or if this listener is setup for every process. IE can any content process just synthesize a login message, and not worry about loading accounts.firefox.com at all?.

And finally FIVE (...I'll come in again...): what about things like cookies/localStorage/Indexeddb etc. Even if a process can't load accounts.firefox.com, do we actually prevent it from setting origin specific data? (I dont think we do, yet, as this is a Fission thing IIRC) From a quick check, it looks like FxA stores a bunch of data in LocalStorage, but changing it doesn't seem to affect state. So maybe this one is not a requirement, but this is an attack to consider.

what about things like cookies/localStorage/Indexeddb

FxA uses localStorage to manage state for signins on the web, but offhand I can't think of a way to use read/write of localStorage for accounts.firefox.com to affect the signed-in state of the browser. I won't 100% rule it out though, there are a lot of different paths through the FxA signin flow.

BTW in comment 12, I'm focusing solve on the "isolate accounts.firefox.com" solution. Taking a step back, I think there are two high level solutions here:

  • isolate accounts.firefox.com so that compromised child process
  • change the security model of Sync, so that accounts.firefox.com can't change the logged in sync user. Ryan's written more along those lines in 1538024)

We might want to consider aligning the solution to 1538007, which is fundamentally the same issue (web origins which have dangerous privileges)

(In reply to Alex Gaynor [:Alex_Gaynor] from comment #0)

UXSS -> privesc via sync (install arbitrary extensions & set arbitrary preferences)

I'm sure there is other interesting settings which lead to a full sandbox escape.

Not a sandbox escape, but rather if a signin can be forced by an attacker, then the attacker can exfiltrate any data already saved on the device that would be synced, e.g., bookmarks and passwords.


(In reply to Ryan Kelly [:rfkelly] from comment #11)

I guess we'd need to simply block iframing accounts.firefox.com for the time being? Would that break anything?

We used to allow certain trusted origins to iframe accounts.firefox.com (via some awful code to generate X-Frame-Options and friends on-the-fly) but I believe all those integrations have been removed. I can't think of any legitimate reason for a site to be loading accounts.firefox.com in an iframe. Shane?

Firefox <= 60 still uses the iframe on the firstrun page. See https://github.com/mozilla/bedrock/issues/6837. We can ask :agibson and team to prioritize switching to the new page.


(In reply to Ryan Kelly [:rfkelly] from comment #3)

I wonder if we have enough information when dispatching the WebChannel message
here [...] to detect that it's coming from an iframe or other unexpected source

:pauljt talked through this with me a bit, and it doesn't sound like there's much we could do here; if the child process is sufficiently compromised then it can make itself indistinguishable from a "good" login page.

I think the right path forward on the FxA front is Bug 1538024, removing the ability for web content to sign the browser in to sync.

As the referred to bug title says, the key is not barring web content from being part of the signin process, but rather allowing FxA to modify the signed in state of Sync "silently". If FxA is in an isolated process and WebChannel messages are sent back to a trusted process, then it seems like the trusted process should be able to show the user some UX confirming the user wants to connect to Sync as user XXX@YYY.ZZZ.

(In reply to Paul Theriault [:pauljt] from comment #12)

(In reply to Ryan Kelly [:rfkelly] from comment #11)

Right, it's not iframing specific -- I just meant there's always two pieces: 1) loading the origin in it's own process, 2) not loading it in any other process :-)

Actually I think there might be THREE things here (cue spanish inquistion intro) :

  1. loading the origin in it's own process
  2. Not loading the the origin in another process
  3. Not loading ANY other web content in the Firefox accounts process

Do I understand correctly that this means only allowing connections to https://accounts.firefox.com from the privileged process? If so, we are going to have to undertake a re-architecture of FxA to move everything to a single domain. Currently https://accounts.firefox.com is only used to serve HTML and do a bit of metrics gathering. Static JS is served from a CDN at https://accounts-static.cdn.mozilla.net. Authentication related calls are made to https://api.accounts.firefox.com, OAuth related calls go to https://oauth.accounts.firefox.com, a user's profile is stored at https://profile.accounts.firefox.com, a user's profile photo is stored at https://firefoxusercontent.com so that invalid images cannot be used as an attack vector.

WebChannel architecture may provides this already though? I'm not sure if the approprate listeners are only created for a process that is loading accounts.firefox.com, or if this listener is setup for every process. IE can any content process just synthesize a login message, and not worry about loading accounts.firefox.com at all?.

IIRC, WebChannel listeners are created at startup and can be sent from any page, however every WebChannel has to specify an originOrPermission from which to accept messages [1]. If a WebChannel message is sent from an unrecognized origin, it's ignored. WebChannels are created on startup to prevent race conditions in channel creation whenever a tab to https://accounts.firefox.com is loaded. I may have this wrong though and WebChannels are created on a per-tab basis. :markh should be able to chim in more here.

From a quick check, it looks like FxA stores a bunch of data in LocalStorage, but changing it doesn't seem to affect state

We do store a user's session state in localStorage, for Firefox Desktop's < 55 [2] and any non-Firefox desktop browser. In Firefox Desktop >= 55, whenever FxA loads we send a WebChannel request to the browser to ask which user is signed in to Sync. If a user is signed in to Sync, then that info will be shared with FxA for subsequent signings. If no user is signed in, then we read the last signed in user from localStorage. For Firefox < 55 and any non Firefox Desktop browser, we always read a user's session state from localStorage.

We also store "verification state" in localStorage, this is used to customize the UX depending on whether a user verifies their email in the same browser in which they sign up in. For OAuth flows, the same verification info is used to help redirect the appropriate tab back to an OAuth relying party whenever a user verifies their email address.

[1] - https://searchfox.org/mozilla-central/rev/56705678f5fc363be5e0237e1686f619b0d23009/toolkit/modules/WebChannel.jsm#155
[2] - https://github.com/mozilla/fxa-content-server/blob/da94f530c8b5bb7477e207944ea0c6623af2f710/app/scripts/lib/channels/web.js#L18

Flags: needinfo?(stomlinson) → needinfo?(markh)

seems like the trusted process should be able to show the user some UX confirming the user wants to connect
to Sync as user XXX@YYY.ZZZ.

We may be able to take this even further, and only show the confirmation for "spontaneous" login attempts that originate from web content, while not showing it when the user is logging in via UI in the browser.

I suspect some users would just click "ok" to this warning message though.

We do store a user's session state in localStorage [...]

Since it's not always clear to folks not steeped in how FxA works, I just want to clarify that the "we" in that last paragraph is "the web content running on accounts.firefox.com".

To be clear, in a site-isolation world, loading JS and other resources from other origins is totally fine, the only thing that'd be problematic is iframes, where you have different contexts code is running in.

I think the ideal outcome here would be to pursue both the site-isolation style special case, and also the changes to prompt if these messages come from web content. That provides us the most resillience.

(In reply to Ryan Kelly [:rfkelly] from comment #16)

seems like the trusted process should be able to show the user some UX confirming the user wants to connect
to Sync as user XXX@YYY.ZZZ.

We may be able to take this even further, and only show the confirmation for "spontaneous" login attempts that originate from web content, while not showing it when the user is logging in via UI in the browser.

Your idea in Bug 1538024 could work well here. If we are going down the OAuth route in the long run, we could go so far as to open FxA with a state token. Pass the state token back to the browser in the login message, and let the browser decide what to do. I'm not sure whether that's any better than tracking principals from which a login message should be accepted w/o extra UI, but hey.. OAuth.

(In reply to Alex Gaynor [:Alex_Gaynor] from comment #17)

To be clear, in a site-isolation world, loading JS and other resources from other origins is totally fine, the only thing that'd be problematic is iframes, where you have different contexts code is running in.

Great! That's much less work than I feared.

I think the ideal outcome here would be to pursue both the site-isolation style special case, and also the changes to prompt if these messages come from web content. That provides us the most resillience.

cc'ing :agibson from mozilla.org to help prioritize removing the last vestiges of the iframe.

(In reply to Shane Tomlinson [:stomlinson] from comment #15)

IIRC, WebChannel listeners are created at startup and can be sent from any page, however every WebChannel has to specify an originOrPermission from which to accept messages [1]. If a WebChannel message is sent from an unrecognized origin, it's ignored. WebChannels are created on startup to prevent race conditions in channel creation whenever a tab to https://accounts.firefox.com is loaded. I may have this wrong though and WebChannels are created on a per-tab basis. :markh should be able to chim in more here.

That's completely correct.

Flags: needinfo?(markh)
No longer blocks: CVE-2019-11741
Depends on: CVE-2019-11741

(In reply to Mark Hammond [:markh] from comment #19)

(In reply to Shane Tomlinson [:stomlinson] from comment #15)

IIRC, WebChannel listeners are created at startup and can be sent from any page, however every WebChannel has to specify an originOrPermission from which to accept messages [1]. If a WebChannel message is sent from an unrecognized origin, it's ignored. WebChannels are created on startup to prevent race conditions in channel creation whenever a tab to https://accounts.firefox.com is loaded. I may have this wrong though and WebChannels are created on a per-tab basis. :markh should be able to chim in more here.

That's completely correct.

So digging into the code to see how this is actually enforced, it looks like this sets up a callback "WebChannelMessageToChrome" which compares the origin that was used for setup against event.principal. This is start, but event in this case is the message (in the messageManager sense), and message.principal. As far as I know, until we do something like fission, there is no way we can trust this, since the data is sent from the child (here maybe? - I might be wrong here, as I would guess webchannel is build on FrameMessageManager not process message manager, but I think its same problem in both.)

So while we do check that the message comes "https://accounts.firefox.com", we just check that against what the child tell us. Until we have some map in place which maps content processes to the set of origins which are supposed to be loaded in that process (which I think is 1491018).

SO for isolation to be of any effect, we either need to do that, and check that the message is coming from a process that should contain 'https://accounts.firefox.com'. OR we need to do process isolation of the blessed loading of accounts.firefox.com, and somehow ONLY listen for WebChannelMessageToChrome that relate to firefox accounts on from this blessed process.

(BTW I hope im wrong here, previously I thought the parent was able to trust message.principal, but now, looking at the code, I cant see how that is true.)

(In reply to Paul Theriault [:pauljt] from comment #21)

(BTW I hope im wrong here, previously I thought the parent was able to trust message.principal, but now, looking at the code, I cant see how that is true.)

Confirmed with Nika, I wasn't wrong, the child process can lie about message.principal.

(In reply to Paul Theriault [:pauljt] from comment #20)

OR we need to do process isolation of the blessed loading of accounts.firefox.com, and somehow ONLY listen for WebChannelMessageToChrome that relate to firefox accounts on from this blessed process.

Nika's suggestion for how we do this without bug 1491018 was to put accounts.firefox.com into a separate process type, and just check that the message comes from the correct process type (ie these types.

Blocks: pwn2own-2019
No longer blocks: CVE-2019-11732
Depends on: CVE-2019-11732

Neha - per comment #23 can your team lead decision making on what the best mitigation for this issue would be?

Flags: needinfo?(nkochar)

(In reply to Selena Deckelmann :selenamarie :selena use ni? pronoun: she from comment #24)

Neha - per comment #23 can your team lead decision making on what the best mitigation for this issue would be?

Discussed this over emails and calls and we believe putting accounts.firefox.com and addonz.mozilla.org in a content process separate from the web content is the best mitigation and this is doable by adding any remote type (like the existing types of 'web', 'file', 'extension',..). Jim's team can work on this while Nika can help with pointers, if needed.

Flags: needinfo?(nkochar)

Jim -- can you prioritize this work, please?

Flags: needinfo?(jmathies)

Seems unlikely to be something we want to uplift to 66. Can we aim for 68/67 here?

(In reply to Liz Henry (:lizzard) (use needinfo) from comment #27)

Seems unlikely to be something we want to uplift to 66. Can we aim for 68/67
here?

We're still trying to get a feel for what's involved.

Flags: needinfo?(jmathies)

I have opened [1] to remove all traces of iframe support in FxA. :agibson and team have opened (and merged) [2] to stop using the iframe in Bedrock. Once bedrock ships their change to prod, we can merge [1], and from our side there should be no more blockers to enabling site isolation for accounts.firefox.com

[1] - https://github.com/mozilla/fxa/pull/773/
[2] - https://github.com/mozilla/bedrock/pull/7023

Great. From the Firefox side we're still trying to get a handle on how much lift that would be, versus just waiting for fission down the line.

I think bug 1538024 is probably the next piece that we should figure out how to make into reality.

Going through this bug I saw the following items that didn't have explicit followups:

From Comment 2: addons.trustedSourceHostnames is used to ensure we don't install addons via sync from skethy sources... but this is a pref and can be overwritten by sync. So.... that seems bypassable. I imagine the correct fix for this is something in Bug 1538015 land though.

Also from Comment 2: these checks only apply on write; not when pulling data from the sync server. This seems like we should take this on as an enhancement, right Ryan?

From Comment 13/15: A malicious content process manipulating localStorage for accounts.firefox.com to affect the Sync signin process. This is the case for < 55 but otherwise seems like a remote possibility right now; but the fix for this is really over in Bug 1484019 rather than hacking on additional checks today. If we can find a specific attack vector for this though we should close it.

Comment 15, 16: We could show a confirmation popup for web content-based sign-ins; even when it comes from the trusted content process. This is what Bug 1538024 is about.

Flags: needinfo?(rfkelly)
Depends on: 1557074
Depends on: 1557651

Also from Comment 2: these checks only apply on write; not when pulling data from the sync server.
This seems like we should take this on as an enhancement, right Ryan?

Yes, thanks for the reminder; I filed Bug 1557651 to follow up on this.

Flags: needinfo?(rfkelly)

Spoke to Tom Ritter this morning. He's going to be the assignee for this issue. The related issue: https://bugzilla.mozilla.org/show_bug.cgi?id=1538015 is being worked on by Mark and the goal is to get it into FF69.

The fix for Bug 1538015 was put into 60(68), 68, and 69. We're going to leave this bug open until Bug 1557074 is resolved at least.

Whiteboard: [adv-main69+][adv-esr68.1+][adv-esr60.9+]

Bug 1557074 is fixed in 71 now - do we wait till 71 hits release or can we close this now?

Flags: needinfo?(tom)

(In reply to Liz Henry (:lizzard) from comment #35)

Bug 1557074 is fixed in 71 now - do we wait till 71 hits release or can we close this now?

While we deployed this in Nightly, it relies on a lot of in-progress Fission work and the underlying code is actually going to be swapped out soon when DocumentChannel is deployed. So we've restricted it to Nightly only at least until DocumentChannel lands. Bug 1578742 is for shipping it out of Nightly, I've added that as a blocker.

Depends on: 1578742
Flags: needinfo?(tom)

We're going to call this Resolved. Bug 1578742 will significantly improve our situation here; but the vulnerability this is about was solved in Bug 1538015.

Status: NEW → RESOLVED
Closed: 4 years ago
Resolution: --- → FIXED
Whiteboard: [adv-main69+][adv-esr68.1+][adv-esr60.9+] → [adv-main69+][adv-esr68.1+][adv-esr60.9+][do not publish until Bug 1538015 is shipped.]
Group: firefox-core-security → core-security-release
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.