CVE-2026-79085
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
network_easter_egg_receivers_for_testingchrome/browser/net/net_error_tab_helper.h |
modified | |
net_error_page_support_for_testingchrome/browser/net/net_error_tab_helper.h |
modified | |
TEST_Fchrome/browser/net/net_error_tab_helper_unittest.cc |
modified | |
BindLambdaForTestingchrome/browser/net/net_error_tab_helper_unittest.cc |
modified |
Files Changed
chrome/browser/net/net_error_tab_helper.ccchrome/browser/net/net_error_tab_helper.hchrome/browser/net/net_error_tab_helper_unittest.cc
Patch
From 32cbdd3b669805a94d63d9bbc5c9bb7d19a44f9f Mon Sep 17 00:00:00 2001 From: Martin Verde <[email protected]> Date: Tue, 30 Jun 2026 18:34:22 -0700 Subject: [PATCH] [NetErrorTabHelper] Restrict NetworkEasterEgg to error documents NetErrorTabHelper handles chrome::mojom::NetworkEasterEgg (GetHighScore / UpdateHighScore / ResetHighScore) on behalf of the network error page so the dino game can persist its high score in the profile. The renderer-side caller (NetErrorHelper) only ever runs inside an error document, including chrome://dino which commits as an ERR_INTERNET_DISCONNECTED error page. Mirror the existing checks in DownloadPageLater() and RunNetworkDiagnosticsHelper() by ignoring requests whose calling frame is not an error document. Adds unit tests covering both the error-page and non-error-page cases, and a test-only accessor for the receiver set following the existing pattern. Bug: 522077127 Change-Id: Iae54cf9fef2752467b221d96fbd1b178e1db3282 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8018324 Reviewed-by: Adam Rice <[email protected]> Commit-Queue: Martin Pan-Verde <[email protected]> Cr-Commit-Position: refs/heads/main@{#1655188} --- diff --git a/chrome/browser/net/net_error_tab_helper.cc b/chrome/browser/net/net_error_tab_helper.cc index 1b2afdcb..a98240f 100644 --- a/chrome/browser/net/net_error_tab_helper.cc +++ b/chrome/browser/net/net_error_tab_helper.cc @@ -23,6 +23,7 @@ #include "content/public/browser/navigation_handle.h" #include "content/public/browser/render_frame_host.h" #include "mojo/public/cpp/bindings/associated_remote.h" +#include "mojo/public/cpp/bindings/message.h" #include "net/base/net_errors.h" #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" #include "url/gurl.h" @@ -312,17 +313,45 @@ #endif // BUILDFLAG(ENABLE_OFFLINE_PAGES) void NetErrorTabHelper::GetHighScore(GetHighScoreCallback callback) { + if (!network_easter_egg_receivers_.CurrentTargetFrame().IsErrorDocument()) { + // IsInMessageDispatch() is checked to avoid calling ReportBadMessage() + // and crashing when unit tests invoke these methods directly. + if (mojo::IsInMessageDispatch()) { + network_easter_egg_receivers_.ReportBadMessage( + "Easter egg high score request from a non-error document"); + } + std::move(callback).Run(0); + return; + } std::move(callback).Run( static_cast<uint32_t>(easter_egg_high_score_.GetValue())); } void NetErrorTabHelper::UpdateHighScore(uint32_t high_score) { + if (!network_easter_egg_receivers_.CurrentTargetFrame().IsErrorDocument()) { + // IsInMessageDispatch() is checked to avoid calling ReportBadMessage() + // and crashing when unit tests invoke these methods directly. + if (mojo::IsInMessageDispatch()) { + network_easter_egg_receivers_.ReportBadMessage( + "Easter egg high score request from a non-error document"); + } + return; + } if (high_score <= static_cast<uint32_t>(easter_egg_high_score_.GetValue())) return; easter_egg_high_score_.SetValue(static_cast<int>(high_score)); } void NetErrorTabHelper::ResetHighScore() { + if (!network_easter_egg_receivers_.CurrentTargetFrame().IsErrorDocument()) { + // IsInMessageDispatch() is checked to avoid calling ReportBadMessage() + // and crashing when unit tests invoke these methods directly. + if (mojo::IsInMessageDispatch()) { + network_easter_egg_receivers_.ReportBadMessage( + "Easter egg high score request from a non-error document"); + } + return; + } easter_egg_high_score_.SetValue(0); } diff --git a/chrome/browser/net/net_error_tab_helper.h b/chrome/browser/net/net_error_tab_helper.h index 03a9cc3..d7627cb 100644 --- a/chrome/browser/net/net_error_tab_helper.h +++ b/chrome/browser/net/net_error_tab_helper.h @@ -114,6 +114,11 @@ return network_diagnostics_receivers_; } + content::RenderFrameHostReceiverSet<chrome::mojom::NetworkEasterEgg>& + network_easter_egg_receivers_for_testing() { + return network_easter_egg_receivers_; + } + content::RenderFrameHostReceiverSet<chrome::mojom::NetErrorPageSupport>& net_error_page_support_for_testing() { return net_error_page_support_; diff --git a/chrome/browser/net/net_error_tab_helper_unittest.cc b/chrome/browser/net/net_error_tab_helper_unittest.cc index 24a2ae6..03aed1c 100644 --- a/chrome/browser/net/net_error_tab_helper_unittest.cc +++ b/chrome/browser/net/net_error_tab_helper_unittest.cc @@ -7,8 +7,12 @@ #include <memory> #include "base/memory/raw_ptr.h" +#include "base/test/bind.h" +#include "chrome/common/pref_names.h" #include "chrome/test/base/chrome_render_view_host_test_harness.h" +#include "chrome/test/base/testing_profile.h" #include "components/error_page/common/net_error_info.h" +#include "components/prefs/pref_service.h" #include "content/public/browser/browser_thread.h" #include "content/public/test/mock_navigation_handle.h" #include "content/public/test/navigation_simulator.h" @@ -69,6 +73,8 @@ void SetCurrentTargetFrame(content::RenderFrameHost* render_frame_host) { network_diagnostics_receivers_for_testing().SetCurrentTargetFrameForTesting( render_frame_host); + network_easter_egg_receivers_for_testing().SetCurrentTargetFrameForTesting( + render_frame_host); net_error_page_support_for_testing().SetCurrentTargetFrameForTesting( render_frame_host); } @@ -77,6 +83,10 @@ return this; } + chrome::mojom::NetworkEasterEgg* network_easter_egg_interface() { + return this; + } + private: // NetErrorTabHelper implementation: @@ -372,10 +382,48 @@ } } +TEST_F(NetErrorTabHelperTest, EasterEggHighScoreOnErrorPage) { + PrefService* prefs = profile()->GetPrefs(); + EXPECT_EQ(0, prefs->GetInteger(prefs::kNetworkEasterEggHighScore)); + + LoadURL(GURL("http://somewhere:123/"), /*succeeded=*/false); + tab_helper()->SetCurrentTargetFrame(web_contents()->GetPrimaryMainFrame()); + + tab_helper()->network_easter_egg_interface()->UpdateHighScore(1000); + EXPECT_EQ(1000, prefs->GetInteger(prefs::kNetworkEasterEggHighScore)); + + uint32_t high_score = 0; + tab_helper()->network_easter_egg_interface()->GetHighScore( + base::BindLambdaForTesting([&](uint32_t score) { high_score = score; })); + EXPECT_EQ(1000u, high_score); + + tab_helper()->network_easter_egg_interface()->ResetHighScore(); + EXPECT_EQ(0, prefs->GetInteger(prefs::kNetworkEasterEggHighScore)); +} + +TEST_F(NetErrorTabHelperTest, NoEasterEggHighScoreOnNonErrorPage) { + PrefService* prefs = profile()->GetPrefs(); + prefs->SetInteger(prefs::kNetworkEasterEggHighScore, 500); + + LoadURL(GURL("http://somewhere:123/"), /*succeeded=*/true); + tab_helper()->SetCurrentTargetFrame(web_contents()->GetPrimaryMainFrame()); + + tab_helper()->network_easter_egg_interface()->UpdateHighScore(1000); + EXPECT_EQ(500, prefs->GetInteger(prefs::kNetworkEasterEggHighScore)); + + uint32_t high_score = 1; + tab_helper()->network_easter_egg_interface()->GetHighScore( + base::BindLambdaForTesting([&](uint32_t score) { high_score = score; })); + EXPECT_EQ(0u, high_score); + + tab_helper()->network_easter_egg_interface()->ResetHighScore(); + EXPECT_EQ(500, prefs->GetInteger(prefs::kNetworkEasterEggHighScore)); +} + #if BUILDFLAG(ENABLE_OFFLINE_PAGES) TEST_F(NetErrorTabHelperTest, DownloadPageLater) { GURL url("http://somewhere:123/"); - LoadURL(url, false /*succeeded*/); + LoadURL(url, /*succeeded=*/false); tab_helper()->SetCurrentTargetFrame(web_contents()->GetPrimaryMainFrame()); tab_helper()->DownloadPageLater(); EXPECT_EQ(url, tab_helper()->download_page_later_url()); @@ -384,7 +432,7 @@ TEST_F(NetErrorTabHelperTest, NoDownloadPageLaterOnNonErrorPage) { GURL url("http://somewhere:123/"); - LoadURL(url, true /*succeeded*/); + LoadURL(url, /*succeeded=*/true); tab_helper()->SetCurrentTargetFrame(web_contents()->GetPrimaryMainFrame()); tab_helper()->DownloadPageLater(); EXPECT_EQ(0, tab_helper()->times_download_page_later_invoked());
Regression Test / PoC
diff --git a/chrome/browser/net/net_error_tab_helper_unittest.cc b/chrome/browser/net/net_error_tab_helper_unittest.cc
index 24a2ae6..03aed1c 100644
--- a/chrome/browser/net/net_error_tab_helper_unittest.cc
+++ b/chrome/browser/net/net_error_tab_helper_unittest.cc
@@ -7,8 +7,12 @@
#include <memory>
#include "base/memory/raw_ptr.h"
+#include "base/test/bind.h"
+#include "chrome/common/pref_names.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "chrome/test/base/testing_profile.h"
#include "components/error_page/common/net_error_info.h"
+#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/mock_navigation_handle.h"
#include "content/public/test/navigation_simulator.h"
@@ -69,6 +73,8 @@
void SetCurrentTargetFrame(content::RenderFrameHost* render_frame_host) {
network_diagnostics_receivers_for_testing().SetCurrentTargetFrameForTesting(
render_frame_host);
+ network_easter_egg_receivers_for_testing().SetCurrentTargetFrameForTesting(
+ render_frame_host);
net_error_page_support_for_testing().SetCurrentTargetFrameForTesting(
render_frame_host);
}
@@ -77,6 +83,10 @@
return this;
}
+ chrome::mojom::NetworkEasterEgg* network_easter_egg_interface() {
+ return this;
+ }
+
private:
// NetErrorTabHelper implementation:
@@ -372,10 +382,48 @@
}
}
+TEST_F(NetErrorTabHelperTest, EasterEggHighScoreOnErrorPage) {
+ PrefService* prefs = profile()->GetPrefs();
+ EXPECT_EQ(0, prefs->GetInteger(prefs::kNetworkEasterEggHighScore));
+
+ LoadURL(GURL("http://somewhere:123/"), /*succeeded=*/false);
+ tab_helper()->SetCurrentTargetFrame(web_contents()->GetPrimaryMainFrame());
+
+ tab_helper()->network_easter_egg_interface()->UpdateHighScore(1000);
+ EXPECT_EQ(1000, prefs->GetInteger(prefs::kNetworkEasterEggHighScore));
+
+ uint32_t high_score = 0;
+ tab_helper()->network_easter_egg_interface()->GetHighScore(
+ base::BindLambdaForTesting([&](uint32_t score) { high_score = score; }));
+ EXPECT_EQ(1000u, high_score);
+
+ tab_helper()->network_easter_egg_interface()->ResetHighScore();
+ EXPECT_EQ(0, prefs->GetInteger(prefs::kNetworkEasterEggHighScore));
+}
+
+TEST_F(NetErrorTabHelperTest, NoEasterEggHighScoreOnNonErrorPage) {
+ PrefService* prefs = profile()->GetPrefs();
+ prefs->SetInteger(prefs::kNetworkEasterEggHighScore, 500);
+
+ LoadURL(GURL("http://somewhere:123/"), /*succeeded=*/true);
+ tab_helper()->SetCurrentTargetFrame(web_contents()->GetPrimaryMainFrame());
+
+ tab_helper()->network_easter_egg_interface()->UpdateHighScore(1000);
+ EXPECT_EQ(500, prefs->GetInteger(prefs::kNetworkEasterEggHighScore));
+
+ uint32_t high_score = 1;
+ tab_helper()->network_easter_egg_interface()->GetHighScore(
+ base::BindLambdaForTesting([&](uint32_t score) { high_score = score; }));
+ EXPECT_EQ(0u, high_score);
+
+ tab_helper()->network_easter_egg_interface()->ResetHighScore();
+ EXPECT_EQ(500, prefs->GetInteger(prefs::kNetworkEasterEggHighScore));
+}
+
#if BUILDFLAG(ENABLE_OFFLINE_PAGES)
TEST_F(NetErrorTabHelperTest, DownloadPageLater) {
GURL url("http://somewhere:123/");
- LoadURL(url, false /*succeeded*/);
+ LoadURL(url, /*succeeded=*/false);
tab_helper()->SetCurrentTargetFrame(web_contents()->GetPrimaryMainFrame());
tab_helper()->DownloadPageLater();
EXPECT_EQ(url, tab_helper()->download_page_later_url());
@@ -384,7 +432,7 @@
TEST_F(NetErrorTabHelperTest, NoDownloadPageLaterOnNonErrorPage) {
GURL url("http://somewhere:123/");
- LoadURL(url, true /*succeeded*/);
+ LoadURL(url, /*succeeded=*/true);
tab_helper()->SetCurrentTargetFrame(web_contents()->GetPrimaryMainFrame());
tab_helper()->DownloadPageLater();
EXPECT_EQ(0, tab_helper()->times_download_page_later_invoked());
Original Bug Report
Potential persistent cross-site tracking via unprotected NetworkEasterEgg Mojo interface
Flapjack, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The chrome::mojom::NetworkEasterEgg Mojo interface is unconditionally registered and bound for all browser frames without validating the frame’s context. A compromised renderer process can use this interface to read and overwrite the prefs::kNetworkEasterEggHighScore profile preference, a syncable, global state. This allows an attacker to embed and retrieve an arbitrary 32-bit identifier across different origins and devices, bypassing standard web isolation boundaries.
Affected files:
chrome/browser/net/net_error_tab_helper.ccchrome/browser/chrome_content_browser_client_receiver_bindings.cc
Estimated timestamp from git blame: 2018-11-28
Description
The NetErrorTabHelper class implements the chrome::mojom::NetworkEasterEgg interface, intended to allow Chromium’s error page “Dinosaur Game” to persist high scores in the user’s profile. However, this interface is exposed to all renderer processes via ChromeContentBrowserClient::RegisterAssociatedInterfaceBindersForRenderFrameHost and bound in NetErrorTabHelper::BindNetworkEasterEgg without any context or origin verification.
Unlike other helper methods (e.g., DownloadPageLater) which strictly assert the frame is displaying an error page (content::PAGE_TYPE_ERROR), the NetworkEasterEgg methods (GetHighScore, UpdateHighScore, ResetHighScore) lack such validation. A compromised renderer can arbitrarily call ResetHighScore() to zero the value, then call UpdateHighScore(unique_id) to embed a 32-bit tracking token.
Because easter_egg_high_score_ is linked to the prefs::kNetworkEasterEggHighScore preference, which is marked as a SYNCABLE_PREF, this value is permanently stored in the user’s unpartitioned profile and synced across devices. A subsequent visit to an attacker-controlled site in a different origin allows another compromised renderer to read this value via GetHighScore(), enabling persistent cross-site tracking.
Potential Exploit Steps
(Note: These are potential steps based on code analysis; a working Proof of Concept has not been executed).
- An attacker compromises the sandboxed renderer process for a standard web page (e.g.,
https://attacker.com). - The compromised renderer requests binding to the
chrome::mojom::NetworkEasterEggassociated interface for its frame. - The browser process unconditionally binds the interface in
NetErrorTabHelper::BindNetworkEasterEgg. - The renderer invokes
ResetHighScore()followed byUpdateHighScore(unique_id), whereunique_idis an arbitrary 31-bit tracking identifier. - The browser process persists this integer in the unpartitioned
prefs::kNetworkEasterEggHighScoreprofile preference. - The user visits a different site (e.g.,
https://target.com), where the attacker again compromises a renderer process. - The second renderer binds the
NetworkEasterEgginterface and callsGetHighScore(), retrieving the embeddedunique_idto reliably track the user across origins.
Suggested Fix
NetErrorTabHelper::BindNetworkEasterEgg, or the individual mojo method handlers (GetHighScore, UpdateHighScore, ResetHighScore), should strictly validate the caller’s context. The interface should only process requests if the calling frame is confirmed to be an error document (e.g., checking entry->GetPageType() == content::PAGE_TYPE_ERROR or similar origin validation). Alternatively, consider partitioning this preference or moving the interface registration entirely out of standard web frames.
Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.