Closed Bug 1739957 (CVE-2022-22759) Opened 3 years ago Closed 3 years ago

Appending elements to sandboxed iframe can trigger javascript execution even if allow-scripts is not set

Categories

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

defect

Tracking

()

VERIFIED FIXED
98 Branch
Tracking Status
firefox-esr91 97+ verified
firefox96 --- wontfix
firefox97 + verified
firefox98 + verified

People

(Reporter: johanaxelcarlsson, Assigned: peterv)

References

()

Details

(Keywords: reporter-external, sec-moderate, Whiteboard: [reporter-external] [client-bounty-form] [verif?][post-critsmash-triage][adv-main97+][adv-esr91.6+])

Attachments

(4 files)

Attached file index.html

This issue was discovered while testing the security of a web application that uses sandboxed iframes to render user supplied HTML as part of a templeting tool. The application assumes (rightfully?) that adding elements to a sandboxed iframe will not trigger any supplied JavaScript. This is the case when using the webapp in Chromium based browsers, but when testing the application in Firefox appending a "broken image XSS payload" to the body of the iframe did trigger JavaScript execution.

If the page contains an iframe such as:

<iframe title="test" id="frame01" sandbox="allow-same-origin" src="temp.html"></iframe>

and a new element is added to the body like this:

let newDiv = document.createElement("div");
newDiv.innerHTML = "<img src='x' onerror='parent.window.alert(document.domain)'>";
frame.contentWindow.document.body.appendChild(newDiv);

The alert box will trigger in Firefox. Important to note is the sandbox attribute allow-same-origin which is needed to be able to write to the iframe at all. Otherwise, writing will be blocked by cross-origin policies. But this attribute should not allow JavaScript execution. The src="temp.html" part is only needed when testing on Chromium as the image will not be added if the body is empty. In Firefox one can omit the src="temp.html" part and still have the JavaScript trigger.

I have tested this on Firefox 94.0.1 (64-bit) on Windows 10 (OS build 19042.1288) and Firefox 94.0.1 on MacOS

I have attached a HTML file to use as a POC. (Or just visit the linked CodePen snippet from the URL section)

POC:

  1. Download the file and put it in a new directory on disk
  2. Start a HTTP server from the directory (for example with the command: python3 -m http.server )
  3. Go to the webpage with Firefox. If started with python http.server the default is http://localhost
  4. The alert box will pop
  5. Go to the same site in Chrome
  6. No alert box will pop

This behavior did, in the case of the mentioned web app, lead to an XSS issue that only appeared in Firefox and led to a compromise of the user account.

I have not digged any deeper into why Firefox does this, but as I read in your guidelines, you wanted researchers to send in bugs as soon as discovered and add context later. I hope that this information is sufficient for now, and please ask me for more information if needed!

Best regards
Johan Carlsson

Flags: sec-bounty?
Group: firefox-core-security → dom-core-security
Component: Security → DOM: Security
Product: Firefox → Core

To add some more context here.

The problem seems to appear when an element from an outer document is appended to the body of a sandboxed iframe. If using the webpage

<html>
<head></head>
<body>
<iframe title="test" id="frame01" sandbox="allow-same-origin" src="temp.html"></iframe>
</body>
</html>

One can test the differences in the console by first running this code snippet:

let frame = document.getElementById("frame01");
let framedoc = frame.contentDocument;
let newDiv = document.createElement("div");
newDiv.innerHTML = "<img src='x' onerror='console.log(document.activeElement)'>";
framedoc.body.appendChild(newDiv);

which will trigger the script to run. Note that I use console.log(document.activeElement) here instead of an alert. The logged output in the terminal will confirm that the script is run in the context of the iframe, as the logged body element will be the body of the iframe.

If instead the new element is created in the iframe document itself, no JavaScript is executed. Test the similar script where the newDiv is created in the frame document

let frame = document.getElementById("frame01");
let framedoc = frame.contentDocument;
let newDiv = framedoc.createElement("div");
newDiv.innerHTML = "<img src='x' onerror='console.log(document.activeElement)'>";
framedoc.body.appendChild(newDiv);

Then nothing will happens. This suggests that the element is evaluated in the correct context (the iframe) but with the restrictions from the original document. As I am completely new to Firefox codebase I have not managed to find the source of this as of yet, but will try to look into it.

I had to switch to repl.io to generate a working POC online for this. You can visit https://draftyfocusedram--joaxcar1.repl.co/ to have one element trigger an alert box and one element not triggering. This happens in Firefox, visiting the site in Chrome will not give any script trigger. The POC source can be found here https://replit.com/@joaxcar1/DraftyFocusedRam

A side note here is that Firefox seems to evaluate the element to append BEFORE it is appended. This can be proven by removing the setTimeout in my POC. This will make the iframe to not have a body to append to. Which will result in the iframe being empty after the append action. Still Firefox will trigger the JavaScript payload in the context of the iframe.

I will continue to investigate this, just thought that I could share the initial findings

/ Johan

A small additional note:

There actually seems to be a race condition in the root of this. When an element is created in a document and it is populated by .innerHTML the element will render even if not appended to any other element in the DOM. So by just executing

let newDiv = document.createElement("div");
newDiv.innerHTML = `<audio src/onerror=console.log(document.activeElement)>`; 

The element will render in the background and execute the onerror payload in the context of the document where it was created. The issue described in this bug report appears when the new element is appended into a new context before this rendering has occurred. And when the rendering finally takes place it will happen in the new context, even if this context is sandboxed. So the above two lines will print the original document body while this

let frame = document.getElementById("frame01");
let newDiv = document.createElement("div");
newDiv.innerHTML = `<audio src/onerror=console.log(document.activeElement)>`; 
frame.contentDocument.body.appendChild(newDiv);

will trigger the script in the context of the iframe and print the iframe document body.

(Also sorry about not using markdown previously)

/Johan

Tricky... When we move the nodes from the parent document into a same-origin sandboxed frame we're looking to the wrong document for the sandbox rules.

NOTE: we should definitely check what happens with a CSP-applied sandbox: they should be the same, but we've found places where they've had to duplicate code and behavior diverges.

Type: task → defect
Component: DOM: Security → DOM: Core & HTML
Flags: needinfo?(peterv)

I have done some initial testing on how this effects CSP settings. I have not tested the CSP version of sandboxing, but if the iframe loads a page that has a CSP meta tag that should refuse all script execution the script is still executed as part of the above mentioned bug.

So the meta tag

<meta http-equiv="Content-Security-Policy" content="script-src 'none'">

in a document loaded like (no sandbox)

<iframe title="test" id="frame01" src="temp.html"></iframe>

will block all forms of JS execution, but will allow the same bug to happen.

See POC at https://replit.com/@joaxcar1/DraftyFocusedRam-1 and in action at https://draftyfocusedram-1.joaxcar1.repl.co/

The POC will load an iframe with the above mentioned CSP and an inline JS execution that will be blocked by the CSP. It will also (as before) create two DIVs from the parent document (index.html) one in the parent document and one directly in the child document. The one created in the parent document and subsequently moved to the child document will trigger JS execution in the context of the child despite the CSP block.

The conclusion is that the same behavior seems to occur for CSP as well as the sandbox attribute

Peter, Nika and I discussed this in a meeting, and it seems like the issue is probably that we compile the event handler for the original context, then recompile it for the new context, but it fails and so we leave around the old one. Peter said he'd take a look at removing the old one, and get Olli to review, in case we're missing something.

Peter was able to reproduce the issue, I think.

Status: UNCONFIRMED → NEW
Ever confirmed: true

Peter also noticed this comment from bz on AddEventHandler: "does that play correctly with nodes being adopted across documents? Need to double-check the spec here."

Assignee: nobody → peterv
Status: NEW → ASSIGNED
Severity: -- → S3
Group: dom-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 3 years ago
Resolution: --- → FIXED
Target Milestone: --- → 98 Branch
Flags: sec-bounty? → sec-bounty+

The patch landed in nightly and beta is affected.
:peterv, is this bug important enough to require an uplift?
If not please set status_beta to wontfix.

For more information, please visit auto_nag documentation.

Flags: needinfo?(peterv)

Comment on attachment 9256916 [details]
Bug 1739957 - Clear event listener. r?smaug!

Beta/Release Uplift Approval Request

  • User impact if declined: Security issue: allows to circumvent sandbox on an iframe.
  • Is this code covered by automated tests?: Yes
  • Has the fix been verified in Nightly?: Yes
  • Needs manual test from QE?: No
  • If yes, steps to reproduce:
  • List of other uplifts needed: None
  • Risk to taking this patch: Low
  • Why is the change risky/not risky? (and alternatives if risky): Not risky, the patch just clears event listeners when moving an element to a different document if something goes wrong while recompiling for that document. Given that compilation fails, there's nothing we can do in the new document anyway for these listeners.
  • String changes made/needed:
Flags: needinfo?(peterv)
Attachment #9256916 - Flags: approval-mozilla-beta?
Attachment #9256917 - Flags: approval-mozilla-beta?

Comment on attachment 9256917 [details]
Bug 1739957 - Clear event listener - test. r?smaug!

Test hasn't landed yet, clearing the approval request.

Attachment #9256917 - Flags: approval-mozilla-beta?

Comment on attachment 9256916 [details]
Bug 1739957 - Clear event listener. r?smaug!

Approved for 97.0b9 and 91.6esr.

Attachment #9256916 - Flags: approval-mozilla-esr91+
Attachment #9256916 - Flags: approval-mozilla-beta?
Attachment #9256916 - Flags: approval-mozilla-beta+
Flags: qe-verify+
Whiteboard: [reporter-external] [client-bounty-form] [verif?] → [reporter-external] [client-bounty-form] [verif?][post-critsmash-triage]
QA Whiteboard: [qa-triaged]

I have reproduced the issue using Firefox Beta 97.0b8 and verified the fix using Firefox Beta 97.0b9 (2022-01-27), Firefox Nightly 98.0a1 (2022-01-28) and Firefox ESR 91.6.0 (2022-01-27) on Windows 10, MacOS 11.6 and Ubuntu 20.04

Status: RESOLVED → VERIFIED
QA Whiteboard: [qa-triaged]
Flags: qe-verify+
Whiteboard: [reporter-external] [client-bounty-form] [verif?][post-critsmash-triage] → [reporter-external] [client-bounty-form] [verif?][post-critsmash-triage][adv-main97+]
Attached file advisory.txt
Whiteboard: [reporter-external] [client-bounty-form] [verif?][post-critsmash-triage][adv-main97+] → [reporter-external] [client-bounty-form] [verif?][post-critsmash-triage][adv-main97+][adv-esr91.6+]
Alias: CVE-2022-22759
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: