Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Preload
DescriptionImproper input validation in Preload
ComponentPreload
Bug ClassLogic Error
Tracker515477007
Fix commit5de9884feeeb (chromium/src) +76/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
components/no_state_prefetch/browser/no_state_prefetch_processor_impl.cc
modified
TEST_F
components/no_state_prefetch/browser/no_state_prefetch_processor_impl_unittest.cc
modified
BindLambdaForTesting
components/no_state_prefetch/browser/no_state_prefetch_processor_impl_unittest.cc
modified

Files Changed

  • components/no_state_prefetch/browser/DEPS
  • components/no_state_prefetch/browser/no_state_prefetch_processor_impl.cc
  • components/no_state_prefetch/browser/no_state_prefetch_processor_impl_unittest.cc
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>();
 
Loading diff…

Regression Test / PoC

shipped with the fix
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>();
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • components/no_state_prefetch/browser/no_state_prefetch_link_manager.cc
  • components/no_state_prefetch/browser/no_state_prefetch_manager.cc
  • components/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:

  1. Interface Binding: NoStatePrefetchProcessorImpl::Create captures the frame’s trustworthy origin using frame_host->GetLastCommittedOrigin() and stores it as initiator_origin_ (components/no_state_prefetch/browser/no_state_prefetch_processor_impl.cc:43).
  2. IPC Dispatch: The renderer provides blink.mojom.PrerenderAttributes, which includes a referrer field (URL and policy).
  3. Validation Failure: In NoStatePrefetchProcessorImpl::Start, the browser validates that the process is allowed to host initiator_origin_, but no validation is performed on the renderer-supplied attributes->referrer->url (components/no_state_prefetch/browser/no_state_prefetch_processor_impl.cc:47).
  4. Navigation Initiation: The unvalidated referrer is passed through NoStatePrefetchLinkManager and NoStatePrefetchManager to NoStatePrefetchContents.
  5. Background Fetch: NoStatePrefetchContents::StartPrerendering creates a hidden WebContents and initiates a navigation using NavigationController::LoadURLWithParams (components/no_state_prefetch/browser/no_state_prefetch_contents.cc:311). The LoadURLParams are populated with the spoofed referrer URL and the trustworthy initiator_origin_.
  6. Network Request: The navigation results in a browser-initiated network::ResourceRequest with credentials_mode set to kInclude. The Network Process sends the request with the forged Referer header.

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.

  1. Gain code execution in a renderer process (e.g., via V8 memory corruption).
  2. Use MojoJS to bind to the blink.mojom.NoStatePrefetchProcessor interface.
  3. Call Start() with PrerenderAttributes configured 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.
  4. Observe the outgoing network request. It is expected to contain the user’s cookies for bank.example and the spoofed Referer header.

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.

View on issue tracker