CVE-2026-79214
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/no_state_prefetch/browser/no_state_prefetch_processor_impl.cc |
modified | |
TEST_Fcomponents/no_state_prefetch/browser/no_state_prefetch_processor_impl_unittest.cc |
modified | |
BindLambdaForTestingcomponents/no_state_prefetch/browser/no_state_prefetch_processor_impl_unittest.cc |
modified |
Files Changed
components/no_state_prefetch/browser/DEPScomponents/no_state_prefetch/browser/no_state_prefetch_processor_impl.cccomponents/no_state_prefetch/browser/no_state_prefetch_processor_impl_unittest.cc
Patch
From 5de9884feeeb83d21c7fb83dd7cc8e35bcb33edd Mon Sep 17 00:00:00 2001 From: Test User <[email protected]> Date: Mon, 06 Jul 2026 15:54:15 -0700 Subject: [PATCH] NoStatePrefetch: validate referrer origin in Start() NoStatePrefetchProcessorImpl::Start() receives a renderer-supplied referrer that is forwarded to the prefetch navigation as the Referer header. Reject the IPC when the referrer URL is not same-origin with the initiator origin that was captured at bind time, in line with the existing NSPPI_* checks. Empty referrers are still allowed. TAG=agy CONV=a230a751-0aa2-42c2-8a06-a68ec987b656 Bug: 515477007 Change-Id: Id2286b73faa76587ca09fc91720e59ad6eab05e1 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8030000 Commit-Queue: Minoru Chikamune <[email protected]> Reviewed-by: Hiroki Nakagawa <[email protected]> Cr-Commit-Position: refs/heads/main@{#1657537} --- diff --git a/components/no_state_prefetch/browser/DEPS b/components/no_state_prefetch/browser/DEPS index 657849b..66bf554 100644 --- a/components/no_state_prefetch/browser/DEPS +++ b/components/no_state_prefetch/browser/DEPS @@ -24,5 +24,6 @@ # RenderFrameHost accessor instead). ".*_unittest\.cc": [ "+services/network/public/cpp", + "+services/network/public/mojom", ], } diff --git a/components/no_state_prefetch/browser/no_state_prefetch_processor_impl.cc b/components/no_state_prefetch/browser/no_state_prefetch_processor_impl.cc index 867e19c..a54651dc 100644 --- a/components/no_state_prefetch/browser/no_state_prefetch_processor_impl.cc +++ b/components/no_state_prefetch/browser/no_state_prefetch_processor_impl.cc @@ -56,6 +56,19 @@ return; } + // The referrer is supplied by the renderer and is forwarded to the prefetch + // navigation, so it must be same-origin with the initiator that bound this + // receiver. + if (!attributes->referrer->url.is_empty() && + !initiator_origin_.IsSameOriginWith(attributes->referrer->url)) { + receiver_.ReportBadMessage("NSPPI_INVALID_REFERRER_ORIGIN"); + // The above ReportBadMessage() closes |receiver_| but does not trigger its + // disconnect handler, so we need to call the handler explicitly + // here to do some necessary work. + Abandon(); + return; + } + // Start() must be called only one time. if (link_trigger_id_) { receiver_.ReportBadMessage("NSPPI_START_TWICE"); diff --git a/components/no_state_prefetch/browser/no_state_prefetch_processor_impl_unittest.cc b/components/no_state_prefetch/browser/no_state_prefetch_processor_impl_unittest.cc index d1ec240..86cd08bc 100644 --- a/components/no_state_prefetch/browser/no_state_prefetch_processor_impl_unittest.cc +++ b/components/no_state_prefetch/browser/no_state_prefetch_processor_impl_unittest.cc @@ -18,6 +18,7 @@ #include "mojo/public/cpp/system/functions.h" #include "net/http/http_response_headers.h" #include "services/network/public/cpp/features.h" +#include "services/network/public/mojom/referrer_policy.mojom.h" #include "third_party/blink/public/common/features.h" #include "third_party/blink/public/common/origin_trials/scoped_test_origin_trial_policy.h" @@ -189,6 +190,67 @@ mojo::SetDefaultProcessErrorHandler(base::NullCallback()); } +TEST_F(NoStatePrefetchProcessorImplTest, StartWithSameOriginReferrer) { + NavigateAndCommit(GURL("https://initiator.test/")); + + auto link_manager = std::make_unique<MockNoStatePrefetchLinkManager>(); + + mojo::Remote<blink::mojom::NoStatePrefetchProcessor> remote; + NoStatePrefetchProcessorImpl::Create( + main_rfh(), remote.BindNewPipeAndPassReceiver(), + std::make_unique<MockNoStatePrefetchProcessorImplDelegate>( + link_manager.get())); + + auto attributes = blink::mojom::PrerenderAttributes::New(); + attributes->url = GURL("https://example.com/prefetch"); + attributes->referrer = + blink::mojom::Referrer::New(GURL("https://initiator.test/page.html"), + network::mojom::ReferrerPolicy::kAlways); + + // Start() call should be propagated to the link manager. + EXPECT_FALSE(link_manager->is_start_called()); + remote->Start(std::move(attributes)); + remote.FlushForTesting(); + EXPECT_TRUE(link_manager->is_start_called()); +} + +TEST_F(NoStatePrefetchProcessorImplTest, StartWithCrossOriginReferrer) { + NavigateAndCommit(GURL("https://initiator.test/")); + + auto link_manager = std::make_unique<MockNoStatePrefetchLinkManager>(); + + mojo::Remote<blink::mojom::NoStatePrefetchProcessor> remote; + NoStatePrefetchProcessorImpl::Create( + main_rfh(), remote.BindNewPipeAndPassReceiver(), + std::make_unique<MockNoStatePrefetchProcessorImplDelegate>( + link_manager.get())); + + // Set up the error handler for bad mojo messages. + std::string bad_message_error; + mojo::SetDefaultProcessErrorHandler( + base::BindLambdaForTesting([&](const std::string& error) { + EXPECT_TRUE(bad_message_error.empty()); + bad_message_error = error; + })); + + auto attributes = blink::mojom::PrerenderAttributes::New(); + attributes->url = GURL("https://example.com/prefetch"); + attributes->referrer = + blink::mojom::Referrer::New(GURL("https://other.test/page.html"), + network::mojom::ReferrerPolicy::kAlways); + + // Start() with a referrer that does not match the initiator origin should be + // reported as a bad mojo message. + ASSERT_TRUE(bad_message_error.empty()); + remote->Start(std::move(attributes)); + remote.FlushForTesting(); + EXPECT_EQ(bad_message_error, "NSPPI_INVALID_REFERRER_ORIGIN"); + EXPECT_FALSE(link_manager->is_start_called()); + // Clean up error handler, to avoid causing other tests run in the same + // process from crashing. + mojo::SetDefaultProcessErrorHandler(base::NullCallback()); +} + TEST_F(NoStatePrefetchProcessorImplTest, Cancel) { auto link_manager = std::make_unique<MockNoStatePrefetchLinkManager>();
Regression Test / PoC
diff --git a/components/no_state_prefetch/browser/no_state_prefetch_processor_impl_unittest.cc b/components/no_state_prefetch/browser/no_state_prefetch_processor_impl_unittest.cc
index d1ec240..86cd08bc 100644
--- a/components/no_state_prefetch/browser/no_state_prefetch_processor_impl_unittest.cc
+++ b/components/no_state_prefetch/browser/no_state_prefetch_processor_impl_unittest.cc
@@ -18,6 +18,7 @@
#include "mojo/public/cpp/system/functions.h"
#include "net/http/http_response_headers.h"
#include "services/network/public/cpp/features.h"
+#include "services/network/public/mojom/referrer_policy.mojom.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/origin_trials/scoped_test_origin_trial_policy.h"
@@ -189,6 +190,67 @@
mojo::SetDefaultProcessErrorHandler(base::NullCallback());
}
+TEST_F(NoStatePrefetchProcessorImplTest, StartWithSameOriginReferrer) {
+ NavigateAndCommit(GURL("https://initiator.test/"));
+
+ auto link_manager = std::make_unique<MockNoStatePrefetchLinkManager>();
+
+ mojo::Remote<blink::mojom::NoStatePrefetchProcessor> remote;
+ NoStatePrefetchProcessorImpl::Create(
+ main_rfh(), remote.BindNewPipeAndPassReceiver(),
+ std::make_unique<MockNoStatePrefetchProcessorImplDelegate>(
+ link_manager.get()));
+
+ auto attributes = blink::mojom::PrerenderAttributes::New();
+ attributes->url = GURL("https://example.com/prefetch");
+ attributes->referrer =
+ blink::mojom::Referrer::New(GURL("https://initiator.test/page.html"),
+ network::mojom::ReferrerPolicy::kAlways);
+
+ // Start() call should be propagated to the link manager.
+ EXPECT_FALSE(link_manager->is_start_called());
+ remote->Start(std::move(attributes));
+ remote.FlushForTesting();
+ EXPECT_TRUE(link_manager->is_start_called());
+}
+
+TEST_F(NoStatePrefetchProcessorImplTest, StartWithCrossOriginReferrer) {
+ NavigateAndCommit(GURL("https://initiator.test/"));
+
+ auto link_manager = std::make_unique<MockNoStatePrefetchLinkManager>();
+
+ mojo::Remote<blink::mojom::NoStatePrefetchProcessor> remote;
+ NoStatePrefetchProcessorImpl::Create(
+ main_rfh(), remote.BindNewPipeAndPassReceiver(),
+ std::make_unique<MockNoStatePrefetchProcessorImplDelegate>(
+ link_manager.get()));
+
+ // Set up the error handler for bad mojo messages.
+ std::string bad_message_error;
+ mojo::SetDefaultProcessErrorHandler(
+ base::BindLambdaForTesting([&](const std::string& error) {
+ EXPECT_TRUE(bad_message_error.empty());
+ bad_message_error = error;
+ }));
+
+ auto attributes = blink::mojom::PrerenderAttributes::New();
+ attributes->url = GURL("https://example.com/prefetch");
+ attributes->referrer =
+ blink::mojom::Referrer::New(GURL("https://other.test/page.html"),
+ network::mojom::ReferrerPolicy::kAlways);
+
+ // Start() with a referrer that does not match the initiator origin should be
+ // reported as a bad mojo message.
+ ASSERT_TRUE(bad_message_error.empty());
+ remote->Start(std::move(attributes));
+ remote.FlushForTesting();
+ EXPECT_EQ(bad_message_error, "NSPPI_INVALID_REFERRER_ORIGIN");
+ EXPECT_FALSE(link_manager->is_start_called());
+ // Clean up error handler, to avoid causing other tests run in the same
+ // process from crashing.
+ mojo::SetDefaultProcessErrorHandler(base::NullCallback());
+}
+
TEST_F(NoStatePrefetchProcessorImplTest, Cancel) {
auto link_manager = std::make_unique<MockNoStatePrefetchLinkManager>();
Original Bug Report
Referer header spoofing in NoStatePrefetch via unvalidated PrerenderAttributes
Project Fortify, 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: A logic flaw in the NoStatePrefetch (legacy prerender) implementation allows a compromised renderer to forge the Referer header in browser-initiated requests. The browser process fails to validate the renderer-supplied referrer URL against the trustworthy initiator origin. This can be used to bypass Referer-based CSRF protections on target sites via background, credentialed GET requests.
Affected files:
components/no_state_prefetch/browser/no_state_prefetch_processor_impl.cccomponents/no_state_prefetch/browser/no_state_prefetch_link_manager.cccomponents/no_state_prefetch/browser/no_state_prefetch_manager.cccomponents/no_state_prefetch/browser/no_state_prefetch_contents.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Summary
A potential security vulnerability in the NoStatePrefetch implementation allows a compromised renderer to spoof the Referer HTTP header in background prefetch requests. The browser process captures the frame’s trustworthy origin but fails to ensure that the renderer-supplied referrer URL in the Start Mojo IPC is consistent with that origin. Because these requests are initiated by the browser in a hidden WebContents, they include the user’s cookies, enabling credentialed CSRF-like attacks.
Technical Details
When a renderer initiates a prefetch (e.g., via <link rel=prerender>), it triggers the blink.mojom.NoStatePrefetchProcessor::Start IPC. The browser handling logic follows this path:
- Interface Binding:
NoStatePrefetchProcessorImpl::Createcaptures the frame’s trustworthy origin usingframe_host->GetLastCommittedOrigin()and stores it asinitiator_origin_(components/no_state_prefetch/browser/no_state_prefetch_processor_impl.cc:43). - IPC Dispatch: The renderer provides
blink.mojom.PrerenderAttributes, which includes areferrerfield (URL and policy). - Validation Failure: In
NoStatePrefetchProcessorImpl::Start, the browser validates that the process is allowed to hostinitiator_origin_, but no validation is performed on the renderer-suppliedattributes->referrer->url(components/no_state_prefetch/browser/no_state_prefetch_processor_impl.cc:47). - Navigation Initiation: The unvalidated referrer is passed through
NoStatePrefetchLinkManagerandNoStatePrefetchManagertoNoStatePrefetchContents. - Background Fetch:
NoStatePrefetchContents::StartPrerenderingcreates a hiddenWebContentsand initiates a navigation usingNavigationController::LoadURLWithParams(components/no_state_prefetch/browser/no_state_prefetch_contents.cc:311). TheLoadURLParamsare populated with the spoofed referrer URL and the trustworthyinitiator_origin_. - Network Request: The navigation results in a browser-initiated
network::ResourceRequestwithcredentials_modeset tokInclude. The Network Process sends the request with the forgedRefererheader.
This behavior allows a compromised renderer to perform invisible, credentialed cross-origin GET requests that appear to originate from a trusted site, potentially bypassing Referer-based CSRF mitigations or forging analytics data.
Potential Reproduction Steps
Note: These steps are based on manual code analysis; a functional PoC has not yet been executed.
- Gain code execution in a renderer process (e.g., via V8 memory corruption).
- Use MojoJS to bind to the
blink.mojom.NoStatePrefetchProcessorinterface. - Call
Start()withPrerenderAttributesconfigured with:url: The target sensitive action URL (e.g.,https://bank.example/transfer-funds).referrer.url: A site trusted by the target (e.g.,https://bank.example/dashboard).referrer.policy:network.mojom.ReferrerPolicy.kAlways.
- Observe the outgoing network request. It is expected to contain the user’s cookies for
bank.exampleand the spoofedRefererheader.
Recommended Fix
In NoStatePrefetchProcessorImpl::Start, validate that the attributes->referrer->url provided by the renderer is either empty or same-origin with the trustworthy initiator_origin_. If the referrer URL is cross-origin, it should be cleared or the message should be treated as a BadMessage.
Evaluated with Chrome root at commit: 29093e11cf509e3593f6229e4b1b075cca356049
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.
- https://source.chromium.org/chromium/chromium/src/+/main:components/no_state_prefetch/browser/no_state_prefetch_contents.cc;l=311
- https://source.chromium.org/chromium/chromium/src/+/main:components/no_state_prefetch/browser/no_state_prefetch_processor_impl.cc;l=43
- https://source.chromium.org/chromium/chromium/src/+/main:components/no_state_prefetch/browser/no_state_prefetch_processor_impl.cc;l=47