PContentParent::SendNotifyVisited broadcasts visited URL's to all content processes, URLs are leaked on renderer compromise
Categories
(Core :: DOM: Navigation, defect)
Tracking
()
People
(Reporter: jan.drescher, Unassigned)
References
Details
(Keywords: privacy, reporter-external, sec-want, Whiteboard: [client-bounty-form])
To facilitate :visited
styling of anchor tags, the Firefox parent process broadcasts (routing id MSG_ROUTING_CONTROL
) a Msg_NotifyVisited
IPC message containing the visited URL to all content processes. Thus, the visited URL is "leaked" to content processes.
The function in question is PContentParent::SendNotifyVisited
in PContentParent.cpp
This bug mainly affects the privacy of victims. An attacker that has compromised the content process can listen for other pages the victim visits while the attackers website is open in the victims browser.
However, this bug also impacts the exploitability of site isolation bypasses like #1899154. Normally, an attacker would not know which web applications the victim actively uses to select targets with relevant login sessions to steal cookies from. In combination with this bug, an attacker can pinpoint which websites the user visits to mount an attack.
The following patch adds a debug log to the content process that logs incoming NotifyVisited
messages. When starting the browser with MOZ_LOG="url_leak_log:5"
and visiting two websites on 127.0.0.1 and 127.0.0.2 one can see that the whole URL of 127.0.0.2 is sent to the content process of 127.0.0.1.
diff --git a/dom/ipc/ContentChild.cpp b/dom/ipc/ContentChild.cpp
index 7245262c9deb..9c8a2f0372e8 100644
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -308,6 +308,8 @@ using namespace mozilla::net;
using namespace mozilla::widget;
using mozilla::loader::PScriptCacheChild;
+static mozilla::LazyLogModule URLLeakLog("url_leak_log");
+
namespace geckoprofiler::markers {
struct ProcessPriorityChange {
static constexpr Span<const char> MarkerTypeName() {
@@ -2303,6 +2305,11 @@ mozilla::ipc::IPCResult ContentChild::RecvNotifyVisited(
if (!newURI) {
return IPC_FAIL_NO_REASON(this);
}
+
+ ::nsCString url_str;
+ newURI->GetSpec(url_str);
+ MOZ_LOG(URLLeakLog, mozilla::LogLevel::Debug, ("Received URL: %s", url_str.get()));
+
auto status = result.visited() ? IHistory::VisitedStatus::Visited
: IHistory::VisitedStatus::Unvisited;
history->NotifyVisited(newURI, status);
<!-- 127.0.0.1:8080/index.html -->
<html>
<h1>
127.0.0.1 Does nothing
</h1>
</html>
<!-- 127.0.0.2:8080/index.html -->
<html>
<script>
window.location.hash = "foo";
</script>
</html>
Fix
There are two ways to fix this: The safest solution would be to isolate link coloring by site, thus disabling the feature.
We researched why we did not observe a similar bug in Chrome. Chrome uses a shared hash map between processes that saves which URLs were visited. Thus, every renderer can query if a given URL was visited, but it is no longer possible to silently listen for visited URLs.
Reporter | ||
Comment 1•2 months ago
|
||
This was discovered in Firefox Linux Version 121. Reproduced in recent Linux build of gecko-dev. The patch applies to commit 2493c256dbff4e3c7e51a7fc61115df887b87e9e
.
Updated•2 months ago
|
Updated•2 months ago
|
Comment 2•2 months ago
|
||
(In reply to Jan Niklas Drescher from comment #0)
We researched why we did not observe a similar bug in Chrome. Chrome uses a shared hash map between processes that saves which URLs were visited. Thus, every renderer can query if a given URL was visited, but it is no longer possible to silently listen for visited URLs.
Thank you for the report. Given that it sounds like you've investigated this, do you have a link handy to the Chrome code that implements this? It would be interesting for us to look at if we decide to implement that.
Reporter | ||
Comment 3•2 months ago
|
||
(In reply to Andrew McCreight [:mccr8] from comment #2)
(In reply to Jan Niklas Drescher from comment #0)
We researched why we did not observe a similar bug in Chrome. Chrome uses a shared hash map between processes that saves which URLs were visited. Thus, every renderer can query if a given URL was visited, but it is no longer possible to silently listen for visited URLs.
Thank you for the report. Given that it sounds like you've investigated this, do you have a link handy to the Chrome code that implements this? It would be interesting for us to look at if we decide to implement that.
The relevant code should be here: https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main/components/visitedlink/
Comment 4•2 months ago
|
||
A quick scan of our code suggests that we do keep track of which processes should be notified about updates, but it does appear that we broadcast to every process in the case where we newly add a URL to the visited set.
I am guessing this is because we don't keep track of which URLs each process has an active interest in (as this could be a lot of information with a large number of links on a page), so any process could have a relevant link.
Looking at Chrome's approach, it appears that Chrome makes the full visited link table available in all processes at all times by mapping it in shared memory (which appears to be writable by the parent process, and read by child processes, though I'm not 100% sure). Chrome also broadcasts visited link updates to child processes. However, unlike Gecko, Chrome's approach is based on a hashed "fingerprint" of the URL in question using a MD5SUM.
As this table is mapped in all processes, it would theoretically be possible for an attacker-controlled process to compute the MD5 fingerprint of each URL they want to check and silently check it against the database. I believe this is the full database, including historical entries It may also be possible for an attacker to transmit the table & salts to check against the full visited table later.
This is improved with partitioned visited links. It appears this swaps out the global salt values with per-origin salts transmitted to only relevant processes, meaning you'd need to reverse a salted MD5 to get visited information for other sites, historical info appears to be always available.
Updated•2 months ago
|
Comment 5•2 months ago
|
||
We improved this behavior in Bug 1714614 but the description in the top comment is accurate from recollection and the description in 1714614.
Chrome's approach of using a shared hash map (it sounds more like a bloom filter to me, but maybe not) is a pretty decent approach to solving the issue.
Updated•2 months ago
|
Comment 6•1 month ago
|
||
The severity field is not set for this bug.
:hsinyi, could you have a look please?
For more information, please visit BugBot documentation.
Comment 7•1 month ago
|
||
We don't like this behavior and consider it a bug we'd like to fix, but presently we don't consider this a security barrier we are actively defending against, so we are de-restricting the bug. In the future we hope to defend this barrier strictly, but presently we generally only give priority to issues that allow an attacker to leak high-value data like cookies or passwords.
Updated•1 month ago
|
Description
•