Low watch CVSS 4.3 chrome Authorization Error 🔧 Commit mapped

Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Preload
DescriptionMissing authorization in Preload
ComponentPreload
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker497499482
Fix commitfcd4f10edbdc (chromium/src) +44/-14
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-29 (per vendor advisory)
Fix landed2026-07-03 (public git)
Disclosed2026-08-25
Patch gap53 days — fix public in source before the advisory

Background

NoStatePrefetch (NSP)
A preloading mechanism that fetches a page and its subresources into a hidden WebContents without executing state-changing script, and, unlike the old prerenderer, is never swapped in to become a visible tab.
`SessionStorageNamespace`
The backing store for a browsing context’s sessionStorage, identified by an id() and shareable across WebContents that belong to the same logical session.
`WebContents::CreateWithSessionStorage` vs `WebContents::Create`
The former binds the new WebContents to a caller-supplied session-storage namespace map, while the latter gives the new WebContents a fresh, independent namespace.
`Matches()`
The NoStatePrefetchContents predicate that decides whether a later navigation in a tab corresponds to an existing prefetch, comparing the URL and the launching tab’s session-storage namespace.

Root Cause Analysis

In NoStatePrefetchContents::StartPrerendering, the hidden prefetch WebContents was created via CreateWebContents(session_storage_namespace), which called WebContents::CreateWithSessionStorage and installed the launching tab’s SessionStorageNamespace (through CreateMapWithDefaultSessionStorageNamespace) into the prefetch contents. As the removed TODO(ajwong) comment records, this was a leftover from the old swap-in prerenderer, where the prerendered contents eventually replaced the tab and therefore needed the tab’s session storage. NSP contents are never swapped in, so the hidden WebContents had no legitimate need for the launcher’s session-storage data yet was granted a live, shared handle to it.

The fix changes CreateWebContents() to call WebContents::Create, giving the prefetch its own isolated namespace, and records the launcher’s namespace id separately only so Matches() still works. This severs the sharing while preserving the matching semantics, as the new browser test confirms by asserting the two namespace ids differ yet Matches() still returns true.

Key insight
The single mistake was carrying over swap-in-era code that handed the launcher tab’s session-storage namespace to a hidden prefetch WebContents that can never be swapped in; the fix isolates the prefetch’s namespace and keeps only the launcher’s namespace id for matching.

Attack Path

  1. Trigger a prefetch A page initiates a NoStatePrefetch for a target URL, causing StartPrerendering to create the hidden prefetch WebContents.
  2. Namespace is shared Because CreateWebContents(session_storage_namespace) used WebContents::CreateWithSessionStorage, the prefetch contents were bound to the launching tab’s SessionStorageNamespace rather than an isolated one.
  3. Improper access to launcher session storage The hidden prefetch WebContents, loading the prefetched document, thereby operated against the launcher tab’s session-storage data instead of its own, which was never intended for a context that is never promoted to a real tab (inferred).
  4. Information exposure/contamination Any read or write of session-storage state performed while loading the prefetch could observe or alter the launcher’s session-storage namespace, constituting missing authorization for that data (inferred; the diff does not demonstrate a concrete cross-origin read primitive).

Impact Assessment

The flaw is a missing-authorization issue in the Preload component: a hidden prefetch WebContents was improperly given a shared handle to the launching tab’s SessionStorageNamespace, allowing session-storage state to be shared with a context that should have been isolated. The advisory classifies it as an AuthorizationError (“Missing authorization in Preload”) and does not report exploitation in the wild; any resulting cross-context data leak or corruption beyond the shared handle itself is (inferred). Preconditions are that a NoStatePrefetch is triggered from a tab so the hidden WebContents is created.

Files Changed

  • chrome/browser/preloading/prefetch/no_state_prefetch/prerender_nostate_prefetch_browsertest.cc
  • components/no_state_prefetch/browser/no_state_prefetch_contents.h
  • components/no_state_prefetch/browser/no_state_prefetch_contents.cc

Audit Directions

  • Legacy swap-in assumptions
    Review remaining prerender/preload code paths for objects that inherit a launcher’s namespaces, cookies, or storage handles under the obsolete assumption they will later be promoted to a live tab.
  • Shared storage namespaces
    Audit all callers of WebContents::CreateWithSessionStorage and CreateMapWithDefaultSessionStorageNamespace to confirm each recipient WebContents genuinely requires the shared namespace rather than an isolated one.
  • Matching vs. access conflation
    Look for cases where an identifier is shared purely to support later matching/lookup but is passed as a live capability handle, granting more access than the matching logic needs.
From fcd4f10edbdcee80d2f510d95b32cb5bfcc3abbb Mon Sep 17 00:00:00 2001
From: Test User <chikamune@google.com>
Date: Fri, 03 Jul 2026 03:16:28 -0700
Subject: [PATCH] Give the prefetch WebContents its own SessionStorageNamespace

NoStatePrefetchContents was passing the launching tab's
SessionStorageNamespace into WebContents::CreateWithSessionStorage. This
was a leftover from the old swap-in prerenderer; NSP contents are never
swapped in, so the hidden WebContents has no need for the launcher's
session storage data and should not share its namespace with another
WebContents. Create the prefetch WebContents with a fresh namespace via
WebContents::Create instead, and keep recording the launcher's namespace
id for use in Matches() only.

Add a browser test that verifies the prefetch WebContents has a distinct
namespace id from the launching tab while still matching it.

TAG=agy
CONV=dbc0ad8c-2f13-498b-81e0-53ee084728bc

Fixed: 497499482
Change-Id: I129d9abcab0fa1661e3b935d681cad42f2266909
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8034880
Commit-Queue: Minoru Chikamune <chikamune@chromium.org>
Reviewed-by: Hiroki Nakagawa <nhiroki@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1656467}
---
diff --git a/components/no_state_prefetch/browser/no_state_prefetch_contents.cc b/components/no_state_prefetch/browser/no_state_prefetch_contents.cc
index a7317c8..42fa693 100644
--- a/components/no_state_prefetch/browser/no_state_prefetch_contents.cc
+++ b/components/no_state_prefetch/browser/no_state_prefetch_contents.cc
@@ -280,7 +280,7 @@
   SetPreloadingTriggeringOutcome(
       attempt_.get(), content::PreloadingTriggeringOutcome::kRunning);
 
-  no_state_prefetch_contents_ = CreateWebContents(session_storage_namespace);
+  no_state_prefetch_contents_ = CreateWebContents();
   no_state_prefetch_contents_->SetOwnerLocationForDebug(FROM_HERE);
   content::WebContentsObserver::Observe(no_state_prefetch_contents_.get());
   delegate_->OnNoStatePrefetchContentsCreated(
@@ -348,14 +348,11 @@
   observer_list_.RemoveObserver(observer);
 }
 
-std::unique_ptr<WebContents> NoStatePrefetchContents::CreateWebContents(
-    SessionStorageNamespace* session_storage_namespace) {
-  // TODO(ajwong): Remove the temporary map once prerendering is aware of
-  // multiple session storage namespaces per tab.
-  return WebContents::CreateWithSessionStorage(
-      WebContents::CreateParams(browser_context_),
-      CreateMapWithDefaultSessionStorageNamespace(browser_context_,
-                                                  session_storage_namespace));
+std::unique_ptr<WebContents> NoStatePrefetchContents::CreateWebContents() {
+  // The hidden WebContents is never swapped in, so it gets its own session
+  // storage namespace rather than sharing the launcher tab's namespace. The
+  // launcher's namespace id is recorded separately for matching.
+  return WebContents::Create(WebContents::CreateParams(browser_context_));
 }
 
 void NoStatePrefetchContents::NotifyPrefetchStart() {
diff --git a/components/no_state_prefetch/browser/no_state_prefetch_contents.h b/components/no_state_prefetch/browser/no_state_prefetch_contents.h
index 985a12e..0fe5909 100644
--- a/components/no_state_prefetch/browser/no_state_prefetch_contents.h
+++ b/components/no_state_prefetch/browser/no_state_prefetch_contents.h
@@ -112,9 +112,10 @@
 
   // Starts rendering the contents in the prerendered state.
   // |bounds| indicates the rectangle that the prerendered page should be in.
-  // |session_storage_namespace| indicates the namespace that the prerendered
-  // page should be part of. |preloading_attempt| allows to log metrics for this
-  // NoStatePrefetch attempt.
+  // |session_storage_namespace| indicates the namespace of the launching tab
+  // and is recorded for use in Matches(); the hidden WebContents is given its
+  // own independent namespace. |preloading_attempt| allows to log metrics for
+  // this NoStatePrefetch attempt.
   virtual void StartPrerendering(
       const gfx::Rect& bounds,
       content::SessionStorageNamespace* session_storage_namespace,
@@ -215,8 +216,7 @@
   void NotifyPrefetchStopLoading();
   void NotifyPrefetchStop();
 
-  std::unique_ptr<content::WebContents> CreateWebContents(
-      content::SessionStorageNamespace* session_storage_namespace);
+  std::unique_ptr<content::WebContents> CreateWebContents();
 
   bool prefetching_has_started_ = false;
 
diff --git a/chrome/browser/preloading/prefetch/no_state_prefetch/prerender_nostate_prefetch_browsertest.cc b/chrome/browser/preloading/prefetch/no_state_prefetch/prerender_nostate_prefetch_browsertest.cc
index e850512d..a553c2d 100644
--- a/chrome/browser/preloading/prefetch/no_state_prefetch/prerender_nostate_prefetch_browsertest.cc
+++ b/chrome/browser/preloading/prefetch/no_state_prefetch/prerender_nostate_prefetch_browsertest.cc
@@ -62,8 +62,10 @@
 #include "content/public/browser/browser_task_traits.h"
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/browsing_data_remover.h"
+#include "content/public/browser/navigation_controller.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/render_process_host.h"
+#include "content/public/browser/session_storage_namespace.h"
 #include "content/public/browser/storage_partition.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/browser/web_contents_observer.h"
@@ -1770,6 +1772,37 @@
   }
 }
 
+// Checks that the hidden WebContents created for a prefetch is given its own
+// SessionStorageNamespace rather than the namespace of the launching tab. The
+// prefetch contents are never swapped in, so they have no need for the
+// launcher's session storage data, and the launcher's namespace should remain
+// available for matching only.
+IN_PROC_BROWSER_TEST_F(NoStatePrefetchBrowserTest,
+                       PrefetchUsesIsolatedSessionStorageNamespace) {
+  GURL url = src_server()->GetURL(kHungPrerenderPage);
+  std::unique_ptr<TestPrerender> prerender =
+      PrefetchFromURL(url, FINAL_STATUS_CANCELLED, 0);
+
+  ASSERT_TRUE(prerender->contents());
+  content::WebContents* prefetch_web_contents =
+      prerender->contents()->no_state_prefetch_contents();
+  ASSERT_TRUE(prefetch_web_contents);
+
+  content::SessionStorageNamespace* launcher_namespace =
+      GetSessionStorageNamespace();
+  ASSERT_TRUE(launcher_namespace);
+  content::SessionStorageNamespace* prefetch_namespace =
+      prefetch_web_contents->GetController()
+          .GetDefaultSessionStorageNamespace();
+  ASSERT_TRUE(prefetch_namespace);
+
+  EXPECT_NE(launcher_namespace->id(), prefetch_namespace->id());
+  EXPECT_TRUE(prerender->contents()->Matches(url, launcher_namespace));
+
+  GetNoStatePrefetchManager()->CancelAllPrerenders();
+  prerender->WaitForStop();
+}
+
 // Cancels the prerender of a page with its own prerender.  The second prerender
 // should never be started.
 IN_PROC_BROWSER_TEST_F(NoStatePrefetchBrowserTest,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/preloading/prefetch/no_state_prefetch/prerender_nostate_prefetch_browsertest.cc b/chrome/browser/preloading/prefetch/no_state_prefetch/prerender_nostate_prefetch_browsertest.cc
index e850512d..a553c2d 100644
--- a/chrome/browser/preloading/prefetch/no_state_prefetch/prerender_nostate_prefetch_browsertest.cc
+++ b/chrome/browser/preloading/prefetch/no_state_prefetch/prerender_nostate_prefetch_browsertest.cc
@@ -62,8 +62,10 @@
 #include "content/public/browser/browser_task_traits.h"
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/browsing_data_remover.h"
+#include "content/public/browser/navigation_controller.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/render_process_host.h"
+#include "content/public/browser/session_storage_namespace.h"
 #include "content/public/browser/storage_partition.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/browser/web_contents_observer.h"
@@ -1770,6 +1772,37 @@
   }
 }
 
+// Checks that the hidden WebContents created for a prefetch is given its own
+// SessionStorageNamespace rather than the namespace of the launching tab. The
+// prefetch contents are never swapped in, so they have no need for the
+// launcher's session storage data, and the launcher's namespace should remain
+// available for matching only.
+IN_PROC_BROWSER_TEST_F(NoStatePrefetchBrowserTest,
+                       PrefetchUsesIsolatedSessionStorageNamespace) {
+  GURL url = src_server()->GetURL(kHungPrerenderPage);
+  std::unique_ptr<TestPrerender> prerender =
+      PrefetchFromURL(url, FINAL_STATUS_CANCELLED, 0);
+
+  ASSERT_TRUE(prerender->contents());
+  content::WebContents* prefetch_web_contents =
+      prerender->contents()->no_state_prefetch_contents();
+  ASSERT_TRUE(prefetch_web_contents);
+
+  content::SessionStorageNamespace* launcher_namespace =
+      GetSessionStorageNamespace();
+  ASSERT_TRUE(launcher_namespace);
+  content::SessionStorageNamespace* prefetch_namespace =
+      prefetch_web_contents->GetController()
+          .GetDefaultSessionStorageNamespace();
+  ASSERT_TRUE(prefetch_namespace);
+
+  EXPECT_NE(launcher_namespace->id(), prefetch_namespace->id());
+  EXPECT_TRUE(prerender->contents()->Matches(url, launcher_namespace));
+
+  GetNoStatePrefetchManager()->CancelAllPrerenders();
+  prerender->WaitForStop();
+}
+
 // Cancels the prerender of a page with its own prerender.  The second prerender
 // should never be started.
 IN_PROC_BROWSER_TEST_F(NoStatePrefetchBrowserTest,
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.