Closed Bug 1764343 (CVE-2024-0747) Opened 3 years ago Closed 1 year ago

iframe CSP bypass via "unsafe-inline" script in parent frame despite iframe CSP having `default-src: none`

Categories

(Core :: DOM: Security, defect, P3)

Firefox 99
defect

Tracking

()

RESOLVED FIXED
122 Branch
Tracking Status
firefox-esr115 122+ fixed
firefox120 --- wontfix
firefox121 --- wontfix
firefox122 + fixed

People

(Reporter: su3604, Assigned: tschuster)

References

(Blocks 1 open bug)

Details

(Keywords: sec-moderate, Whiteboard: [adv-main122+][adv-esr115.7+])

Attachments

(3 files)

User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36

Steps to reproduce:

  1. Set the following files on your web server.
<html>
    <head>
        <meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-inline'">
    </head>
    <body>
        <iframe id=x src='self.html'></iframe>
        <script nonce=123>
            x.onload=_=>x.contentDocument.write('<script>alert(1)<\/script>');
        </script>
    </body>
</html> 
<html>
    <head>
        <meta http-equiv="Content-Security-Policy" content="default-src 'none'">
    </head>
    <body>
    </body>
</html>
  1. Go to the test.html page

Actual results:

The inline script written in the iframe is executed.

Expected results:

CSP is defined for each page. That is, when specifying a separate page (eg, self.html) other than the local scheme (https://www.w3.org/TR/CSP3/#security-inherit-csp) in the iframe src, the iframe must be affected by the corresponding CSP, not the parent CSP. However, I confirmed that the CSP of the parent page is inherited to another separate page when writing is performed by calling the x.contentDocument.write function in Firefox.

Although self.html should not allow inline script execution according to its CSP default-src 'none', an attacker can use the trick in this report to cause inline scripts to be executed in self.html environment.

FYI, I've seen Chrome, Edge, and Safari correctly enforce the CSP of the child page in this case.

Group: firefox-core-security → dom-core-security
Component: Untriaged → DOM: Security
Product: Firefox → Core
Summary: [CSP bypass] → iframe CSP bypass via "unsafe-inline" script in parent frame despite iframe CSP having `default-src: none`
Status: UNCONFIRMED → NEW
Ever confirmed: true
Keywords: sec-moderate

I'll take a couple of minutes later this week to better understand how and why the CSP is inherited.

Flags: needinfo?(fbraun)

If the parent page had reached in and added an inline onclick handler or whatever, that should get blocked.

If the CSP were in the HTTP header it should have stuck around.

I think in this case (have not experimented yet), the document.write() implies a document.open() which blows away the current document (where the CSP was set in a META tag) and then you're left without a CSP. Maybe it would inherit from the parent, but that allows unsafe-ineline.

Freddy will investigate

Severity: -- → S3
Priority: -- → P3
Whiteboard: [domsecurity-backlog1]

OK, I took a moment to look at this. To reproduce this, I have modified the child document to have a big headline (<h1>Child</h1>) would suffice.
Loading the test case will verify Dan's assumption in comment 3: The self.html document will be thrown away completely and the document.write() call creates a new one (the headline goes away). The new document doesn't have a <meta> CSP and the script will be allowed.

Contrary, if the parent script did the following instead:

s = x.contentDocument.createElement("script");
s.innerText = "alert(2)";
x.contentDocument.body.append(s);

Then the <meta> element would still be intact and therefore disallow the inline script:

Content Security Policy: The page’s settings blocked the loading of a resource at inline (“default-src”).

I think we can fix this as INVALID, but I'm happy to hear objections.

Flags: needinfo?(fbraun)

(In reply to Frederik Braun [:freddy] from comment #4)

I think we can fix this as INVALID, but I'm happy to hear objections.

(In reply to Seongil Wi from comment #1)

FYI, I've seen Chrome, Edge, and Safari correctly enforce the CSP of the child page in this case.

I'm curious why other browsers would behave differently here, if the spec says we're doing the right thing?

Flags: needinfo?(fbraun)

Huh, it looks like the CSP should be copied over but never is. Or does it copy it from the opener (instead of from the previous popup document?)

Maybe that changed with recent document/navigation changes?
https://searchfox.org/mozilla-central/rev/b0844a16d60042c8e1e8f50465a89c1696d283fb/dom/base/Document.cpp#9535-9544

I'll keep the needinfo to continue investigating. Sorry this has been taking so long.
I'm also CCing Tom who has done most of the recent CSP work. Not necessarily because I expect him to pick this up, but because I'm sure this will be interesting to him :)

I believe this is a dupe of bug 1754872, where both Mike West and Anne think the spec supports the Chrome behavior. document.open() is not creating an actual fresh "about:blank" document. They interpret https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#document-open-steps as emptying out the document but then re-using it rather than creating a new document. That would preserve the "policy container" attributes including CSP.

Similarly to what is happening in Bug 1754872, when initially parsing and binding the self.html iframe's document we parse the Meta tag CSP directives and apply them to the iframe's document.

Later, when we are in the implicit document.open we inherit the opening document's CSP directives. We replace the existing DOM in the iframe's document with the <script> tag passed into the subsequent document.write call and when binding the <script> tag to the tree call CSPAllowsInlineScript which in turn calls nsCSPContext::GetAllowsInline.

Here mPolicies has a single entry, which is the inherited script-src 'unsafe-inline' directive from the opening document, but doesn't retain the Meta tag CSP directive previously assigned from self.html so ultimately allows the script to run.

Based on both of these bugs and their test cases and comments it seems like inheriting the CSP directives from the opener isn't what we actually want for a same-origin iframe and that we should be inheriting the iframe's CSP directives from before calling document.open? Or is the current behavior what we want and compliant with the spec?

I'm not familiar with any of the relevant specs, however, so I'm needinfoing Tom S. and Christoph to help me with guidance. I'm also attaching the test case provided by Seongil as a mochitest to help debug if needed

Assignee: nobody → jewilde
Status: NEW → ASSIGNED
Flags: needinfo?(tschuster)
Flags: needinfo?(fbraun)
Flags: needinfo?(ckerschb)
Whiteboard: [domsecurity-backlog1] → [domsecurity-active]

right, we currently, incorrectly, treat document.write() on an existing document (or more specifically, the implicit document.open() that happens on the first write) as if we navigated to "about:blank" and built a new document from scratch. At least, from the CSP and probably sandbox POV, if not in all HTML/DOM particulars.

These rules will apply to any two existing same-origin documents where a script running in one (AD for active document) modifies the other (TD for target document) through a window reference. Could be a parent document and an iframe, a window and a popup it opened, or a popup writing back into its opener. The Target Document will have policies associated with it, in this case we're talking about CSP, and those existing policies should be preserved; it should not inherit ANY policy from the AD that re-wrote it using document.write(), just as it wouldn't inherit anything if AD modified TD through pure DOM node deletion/creation.

If the newly written content has a <meta CSP> in it that gets parsed then that should be added to the list of TD policies, but we should not remove or replace any that already existed even if they were from <meta> tags instead of headers. If we allow document.write() to erase the policy we created from previous <meta> tag content then this could be used by a document to relax it's own CSP.

Flags: needinfo?(ckerschb)

Our Document::Open() code that you reference in comment 8 has comments aligning each bit of code with a step of the spec -- except the bit you pointed to! Clearly that was added to solve some inheritance problem we were having, but it's causing this one. We can't simply remove it without figuring out why it was there initially. Is this where we inherit policies because fresh "about:blank" documents happen to go through here too? If so it should be moved to some earlier document creation time. Or maybe we've done it there, too, by now and this is "extra"?

Likewise, the placement of this securityInfo bit is suspicious:
https://searchfox.org/mozilla-central/rev/1865e9fba4166ab2aa6c9d539913115723d9cc53/dom/base/Document.cpp#9806-9808

That, too, looks like something that should happen on document creation, not document.open(), and not get reset when manipulated from the outside. I think it's not harmful here since this can only happen in a same-origin scenario, but I can imagine some edge-case scenarios where the values would be technically wrong (but not meaningfully so). for example: Long-lived tab where, after putting your laptop to sleep and commuting home, you interact with framed content in a way that causes a same-site navigation or reload. Maybe one of those locations uses a MITM security proxy (Enterprise middlebox, or home anti-virus scanning) so you'd get a different certificate chain and ciphersuite details. Wouldn't be that important -- presumably you'd get the same values for the parent page if you had refreshed it.

Mostly I mention it because it seems to come from the same mental-model as the CSP inheritance problem we're having, and may mean there wasn't a better place for it at the time. Hopefully there is now, but if not you may need to create one.

I've updated the patch to remove that block of code in Document::Open which seems to most likely have been code that either got duplicated or inadvertently left behind in past refactors of the CSP code. I believe it was once doing what this code block is doing now, although trying to track it down past the changes from Bug 965637 has been near impossible.

I've also tested these changes out pretty extensively on try which only came back with known intermittents. The last thing I need to do is write some more comprehensive tests and then this will be ready for review! Thanks Dan for your help, I greatly appreciate it

I wasn't really aware of how document.write works (yuck!). It seems a bit unfortunate that we are going to start relying on that apparently non-standard behavior with about:blank, which is what makes CSP_ShouldResponseInheritCSP return true.

Flags: needinfo?(tschuster)
Attachment #9357678 - Attachment description: WIP: Bug 1764343 - CSP inheritance from document.write; → Bug 1764343 - Remove steps in Document::Open not derived from spec; r=dveditz
Assignee: jewilde → nobody
Status: ASSIGNED → NEW
Whiteboard: [domsecurity-active]

It seems a bit unfortunate that we are going to start relying on that apparently non-standard behavior with about:blank

document.open()/write() is historical behavior that the WHATWG has very much attempted to document/standardize.

It seems this is reviewed and ready to go. I don't recall what was keeping us from landing? Maybe a green try run?
Can you kick this over the finish line, Tom?

Flags: needinfo?(tschuster)
Assignee: nobody → tschuster
Flags: needinfo?(tschuster)

Looks like the tests need to be updated still.

Please have a look at the review.

Flags: needinfo?(dveditz)
Flags: needinfo?(dveditz)
Pushed by tschuster@mozilla.com: https://hg.mozilla.org/integration/autoland/rev/5bfc7845071c Remove steps in Document::Open not derived from spec; r=dveditz
Group: dom-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 1 year ago
Resolution: --- → FIXED
Target Milestone: --- → 122 Branch
Blocks: 1869940
QA Whiteboard: [post-critsmash-triage]
Flags: qe-verify-

Is this ready for an ESR approval request?

Flags: needinfo?(tschuster)

Any concerns with an ESR approval request?
115.7esr builds next week.

Comment on attachment 9371286 [details]
Bug 1764343 - ESR115: Remove steps in Document::Open not derived from spec. r?dveditz

ESR Uplift Approval Request

  • If this is not a sec:{high,crit} bug, please state case for ESR consideration: CSP is not properly being applied and could allow (among others) unexpected script execution on websites.
  • User impact if declined:
  • Fix Landed on Version: 122
  • Risk to taking this patch: Low
  • Why is the change risky/not risky? (and alternatives if risky): On Beta already and matches (tested) Chrome behavior.
Flags: needinfo?(tschuster)
Attachment #9371286 - Flags: approval-mozilla-esr115?

Comment on attachment 9371286 [details]
Bug 1764343 - ESR115: Remove steps in Document::Open not derived from spec. r?dveditz

Approved for 115.7esr.

Attachment #9371286 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+
Whiteboard: [adv-main122+][adv-esr115.7+]
Alias: CVE-2024-0747

Making Firefox 122 security bugs public. [bugspam filter string: Pilgarlic-Towers]

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: