Closed Bug 1665820 (CVE-2020-26967) Opened 4 years ago Closed 4 years ago

Screenshot exception with mutation observers

Categories

(Firefox :: Screenshots, defect)

defect

Tracking

()

VERIFIED FIXED
83 Branch
Tracking Status
firefox-esr78 --- wontfix
firefox82 --- wontfix
firefox83 --- fixed
firefox84 --- verified

People

(Reporter: dveditz, Assigned: Gijs)

References

Details

(Keywords: reporter-external, sec-low, Whiteboard: [post-critsmash-triage][adv-main83+])

Attachments

(6 files, 1 obsolete file)

Filing this for kaisersoze, WIP for a screenshot bug. Currently if you take a screenshot of the testcase page using one of the two buttons in the top right (whole page or visible window) it will throw an exception in the extension itself. Seems to be trying to grab the screenshot iframe and getting there too early, but could be close to something like a priv-esc into the extension context if he gets the timing right.

Here is a revised testcase, that shows in web console that the contents of the screenshot extension become accessible. It's working for me locally on release, right now the screenshot extension doesn't even load for me in nightly.

To see it work, right click choose screenshot, and then open the web console.

If I would have had time, I planned on hijacking the event listeners possibly gaining access to the browser part of the extension. From there we would see. I believe i may not have the time though.

This only accesses the content that is injected into the page, right? Unfortunately there's no way for extensions to do better so they can't really trust their stuff once it's in the page. See bug 1340930.

Of course, if the extension trusts it's injected content unwisely that can certainly lead to a security bug, but simply accessing the DOM bits aren't that.

[Sidenote: if possible, it would help if your testcases had basic instructions in them. Like in this revised testcase there's a button that says "Click me" but clicking it is not part of the test. If it were part of the test it is not clear if it was to be clicked before or after the screenshot, or really that screenshotting is involved at all.]

I've cc'd you on bug 1414937 that results from that because it seems similar to what you've done here. Also on bug 1389707 which is a spoof built on top of it. Those are known issues. A novel result would be getting the extension code itself to do something unexpected, or injecting script into the extension context (not content scripts).

See Also: → 1340930

The button and elements are there for the fact that the preview iframe will not initialize unless it has something to preview(ie elements).

I'll do more on this if it's not a security issue, then leave it as be and I will follow up and prove it. There's also two other possible attack vectors but I'll leave those for the future.

Let me clarify, IMHO patching this would be proof of violation of defense in depth. If this was not to be patched(I think that's what I prefer) then I'm sure with more time I can access the execution scope of the extension itself. The two other iframes contain a final check to verify that in fact they have properly loaded the extension URL, the preview iframe does not include this check for some reason(not sure why).

Either way is fine with me, I have a lot going on but will get back to this ASAP. You guys just let me know.

Forgot to flag for bounty consideration.

Flags: sec-bounty?

Some follow up notes, might help people understand what is going on here. A mutation observer is being used to detect when iframes are added to a page for screenshots(nothing new). After this the preview iframe is being selected because for some reason it lacks a final check that the correct extension URI is loaded, then the srcdoc property of that iframe is being set to "<html></html>" which provides a blank document as the extension expects.

When srcdoc and the src of an iframe are both set, srcdoc overwrites the src attribute, and loads the about:srcdoc URL into the window. The window remains content accessible. After this is done to the screenshot preview iframe, it lacks a final check to see that in fact it is at the expected extension URL(which the selection and preselection iframes both have). If in fact there are visible elements in the content page to preview, it initializes the preview iframe, and injects it's content into it, which in this case is completely accessible by content.

What appeared to be the timing issue from before was actually caused by the lack of visible elements. Sorry for the button that says click me, copy pasta rushing through work.

(In reply to Kaizer Soze from comment #5)

Let me clarify, IMHO patching this would be proof of violation of defense in depth. If this was not to be patched(I think that's what I prefer) then I'm sure with more time I can access the execution scope of the extension itself.

So I'm confused - the execution scope of the extension is mostly in a different process. Short of the extension using eval or similar, I don't see how you'd do this? Even the same-process bits (which do exist, just... there's less of it, and communication is via message passing) I'd expect to be insulated via the usual JS context and wrapper machinery.

It'd surprise me less if you could use this to cajole the extension into creating the "wrong" screenshot, or something. And that's not great, but isn't really the same as accessing the execution scope of the extension, right? Can you elaborate?

The two other iframes contain a final check to verify that in fact they have properly loaded the extension URL, the preview iframe does not include this check for some reason(not sure why).

I agree this is odd.

Emma/Sam: CC'ing you as a heads-up (also CC'd you to the other bugs Dan mentioned).

Flags: needinfo?(kaizersoze915)

Once you have access to the elements, there's always a possibility of code execution within the context based on the fact that there is a lot going on, that depends on text interaction on the elements(style changes considerably). I was aiming for elements that have event listeners, not they're vulnerable per say. I just had a hunch this could be much bigger.

There's @SubstitutedCSS and a few others things that get directly inlaid to HTML(given I have not confirmed they're able to be influenced externally, or apply to the preview iframe.)

There's more there, just not for me at the moment. Take this as is for now. I'll see what I can do in the meantime.

I have in fact looked over and understood the event listeners being mapped with a sort of wrapper function and SendEvent being used to communicate messages. Take it and work on it as is, as my current situation is less than encouraging especially with everything else going on in the world.

Flags: needinfo?(kaizersoze915)

Sorry if anything is off there. sendEvent substitutedCss etc. I was working from memory, back into the code some now. I can now confirm this also works on nightly, but I assume you already knew that. I will follow up with any updates.

I'm not seeing a way to exploit this right now, but I do see a patch to put in place for this particular case. Simple one liner. Gijs, do you know who maintains this? I need to ask if there's a reason for using an onload handler on the preview iframe with iframe.onload instead of using addEventListener?

The use of assertIsBlankDocument, and using addEventListener instead of iframe.onload would lock down anything I could see to grasp at. I can do the patch, just give me a little time to see if it breaks things(will be later).

Also, is there a reason that assertIsTrusted is using macro expansion on an untrusted event? As in

const exc = new Error(Received untrusted event (type: ${event.type}));

Since the event isn't trusted, then the type might not be what is normally expected.

Flags: needinfo?(gijskruitbosch+bugs)

(In reply to Kaizer Soze from comment #11)

I'm not seeing a way to exploit this right now, but I do see a patch to put in place for this particular case. Simple one liner. Gijs, do you know who maintains this? I need to ask if there's a reason for using an onload handler on the preview iframe with iframe.onload instead of using addEventListener?

I don't know, off-hand I wouldn't have thought so. Equally, I'd expect both the onload setter and calls to addEventListener to be protected by wrapper handling (but I haven't checked). You could manually invoke the onload function, maybe, but then you could also manually dispatch an event with dispatchEvent (and write a custom class MyEvent extends Event { get type() { /* evil things here */ } } ) if so inclined, right?

The use of assertIsBlankDocument, and using addEventListener instead of iframe.onload would lock down anything I could see to grasp at. I can do the patch, just give me a little time to see if it breaks things(will be later).

Also, is there a reason that assertIsTrusted is using macro expansion on an untrusted event? As in

const exc = new Error(Received untrusted event (type: ${event.type}));

Since the event isn't trusted, then the type might not be what is normally expected.

But both event.isTrusted and event.type are properties on the DOM event so wrappers should get us the "real" DOM event property instead of any potential shadowing from a "fake" event. From a quick, non-screenshots-related test, this appears to work correctly from a chrome-privileged calling context and a non-chrome-privileged event dispatcher. It's possible that if the extension's interactions with the page happen in a scope that doesn't have a different privilege level (so same principal associated) that then there's no wrapper protection - but equally, in that case "hijacking" the extension script gives no greater power than what the page script could do itself anyway.

Flags: needinfo?(gijskruitbosch+bugs) → needinfo?(kaizersoze915)

Ah, hm, so I guess the onload setter means that you can then directly invoke the handler via node.onload("foo") with arbitrary arguments, whereas dispatchEvent enforces that the object you pass is an Event, which gets the relevant wrapper.

Though again, I'd expect that wrappers for the different principals enforce that the relevant object we get passed have to be structured-clonable, so custom getters get lost?

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

Though again, I'd expect that wrappers for the different principals enforce that the relevant object we get passed have to be structured-clonable, so custom getters get lost?

This is in fact the case, or at least the function is a wrapper denying all access. I'm just wondering about the choice to use iframe.onload for the preview iframe where the selection, and pre-selection iframes use addEventListener? Is it necessary for screenshots to function properly?

It's sad to say that I am not up on Phabricator, or a properly integrating eslint editor. I am attempting to prepare a patch to stop this specific case here. I will see if I have time to test the difference in event handlers.

As a side note, I'm always weary of anything that involves Error, or an exception as sometimes they're are given to confusion between real objects and wrappers.

Sorry Gijs, I'm more than a bit out of touch with all the changes that have been made in recent times. More testing is needed from my end.

Flags: needinfo?(kaizersoze915)
Attached patch ui.js.patchSplinter Review

Here is the most I'm going to have time for at the moment. I have no idea who to flag for review, or what else needs to be done at this point. Sorry for things being so rough, but this is the best I can bring to the table at the moment.

(In reply to Kaizer Soze from comment #15)

Created attachment 9177460 [details] [diff] [review]
ui.js.patch

Here is the most I'm going to have time for at the moment. I have no idea who to flag for review, or what else needs to be done at this point. Sorry for things being so rough, but this is the best I can bring to the table at the moment.

No worries, I'll self-needinfo to get this into phab "soon" (it's late here)

Flags: needinfo?(gijskruitbosch+bugs)
Summary: Screenshot exception with mutation observers [WIP] → Screenshot exception with mutation observers

There may be more that could be done here in terms of things we would not want to see, but I'll test that later as that patch should stop what I have in mind anyway. If I think of anything else, or stumble onto something I'll update this.

I was a little puzzled by bug 1660651, but now that that's fixed...

Flags: needinfo?(gijskruitbosch+bugs)
Attached file Bug 1665820, r?emalysz
Assignee: nobody → gijskruitbosch+bugs
Status: NEW → ASSIGNED

I credited you as patch author as "Kaizer Soze <kaizersoze915@gmail.com>" - is that fine or would you like me to put something else?

Flags: needinfo?(kaizersoze915)
Attached file Bug 1665820 - tests, r?emalysz (obsolete) —

Depends on D92589

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

I credited you as patch author as "Kaizer Soze <kaizersoze915@gmail.com>" - is that fine or would you like me to put something else?

I'd like to land this by the end of this week to avoid it sitting unaddressed longer than necessary, so I'm going to go ahead with these credits unless I hear otherwise by Friday UTC. I'll try and ping you out-of-band.

That should be fine. Thanks man.

Flags: needinfo?(kaizersoze915)

This needs a sec-rating before landing. Going with sec-low as per a quick skim and Gijs' suggestion.
Nice find, though!

Keywords: sec-low
Blocks: 1671119

Comment on attachment 9179884 [details]
Bug 1665820 - tests, r?emalysz

Revision D92606 was moved to bug 1671119. Setting attachment 9179884 [details] to obsolete.

Attachment #9179884 - Attachment is obsolete: true

Could someone put a sec-bounty flag on this. I know it's a low, but it's good work IMHO.

(In reply to Kaizer Soze from comment #27)

Could someone put a sec-bounty flag on this. I know it's a low, but it's good work IMHO.

The flag is already set. :-)

Group: firefox-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 4 years ago
Resolution: --- → FIXED
Target Milestone: --- → 83 Branch
Flags: qe-verify+
Whiteboard: [post-critsmash-triage]

Could you please provide some steps to reproduce in order to verify this issue just not to miss any details?
Thanks.

Flags: needinfo?(gijskruitbosch+bugs)

(In reply to Hani Yacoub from comment #30)

Could you please provide some steps to reproduce in order to verify this issue just not to miss any details?
Thanks.

  1. open https://bugzilla.mozilla.org/attachment.cgi?id=9176739 ("revision" testcase from this bug)
  2. click the "click me" button
  3. open the devtools web console
  4. clear the console
  5. in the page action menu, click "Take a Screenshot"
  6. wait a few seconds
  7. there should not be a console log statement that looks like this:

HTMLAllCollection { 0: html, 1: head, 2: link, 3: style, 4: title, 5: body, 6: div.preview-overlay, 7: div.preview-image, 8: div.preview-buttons, 9: button.highlight-button-cancel, … }

Flags: needinfo?(gijskruitbosch+bugs) → needinfo?(hani.yacoub)

Thank you for the steps.
What I'm seeing now on the latest Firefox Nightly is a shorter array HTMLAllCollection { 0: html, 1: head, 2: body, length: 3 } and the security error appears in the console after canceling the screenshot. (please see the screenshot attached, on the left is Firefox Nightly 82.0a1 (2020-09-18) and on the right is the latest Nightly)
I'm not sure if this the expected behaviour here.

Flags: needinfo?(hani.yacoub) → needinfo?(gijskruitbosch+bugs)
Flags: sec-bounty? → sec-bounty+

(In reply to Hani Yacoub from comment #32)

Created attachment 9182385 [details]
Screenshot 2020-10-19 at 15.44.42.png

Thank you for the steps.
What I'm seeing now on the latest Firefox Nightly is a shorter array HTMLAllCollection { 0: html, 1: head, 2: body, length: 3 } and the security error appears in the console after canceling the screenshot. (please see the screenshot attached, on the left is Firefox Nightly 82.0a1 (2020-09-18) and on the right is the latest Nightly)
I'm not sure if this the expected behaviour here.

Sorry for the delay - yes, this is expected behaviour - the difference is that the screenshots UI has not injected anything into the dummy document created by the exploit/testcase

The other thing you can/should check is that the following error appears in the browser console (need to have "show content messages" turned on):

Error: iframe URL does not match expected blank.html

which also indicates the fix is working.

Flags: needinfo?(gijskruitbosch+bugs) → needinfo?(hani.yacoub)

The other thing you can/should check is that the following error appears in the browser console (need to have "show content messages" turned on):

Error: iframe URL does not match expected blank.html

which also indicates the fix is working.

I'm not sure for the error received in the console (See attachment from comment 32) where should I see if "show content messages" is turned on?

Flags: needinfo?(hani.yacoub) → needinfo?(gijskruitbosch+bugs)

(In reply to Hani Yacoub from comment #34)

The other thing you can/should check is that the following error appears in the browser console (need to have "show content messages" turned on):

Error: iframe URL does not match expected blank.html

which also indicates the fix is working.

I'm not sure for the error received in the console (See attachment from comment 32) where should I see if "show content messages" is turned on?

This error is in the browser console (open with ctrl/cmd-shift-j), not the regular devtools console. In the browser console there's a "cog"/gear icon in the top right which brings up a menu that has an option to "show content messages".

Flags: needinfo?(gijskruitbosch+bugs) → needinfo?(hani.yacoub)

Thank you for the details.
Verified as fixed on Firefox Nightly 84.0a1 (2020-10-22) on Windows 10 x64, MacOS 10.14 and on Ubuntu 20.04 x64.

Status: RESOLVED → VERIFIED
Flags: needinfo?(hani.yacoub)
Whiteboard: [post-critsmash-triage] → [post-critsmash-triage][adv-main83+]
Attached file advisory.txt
Alias: CVE-2020-26967
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: