CVE-2026-79184
Overview
Background
- NoStatePrefetch (NSP)
- A preloading mechanism that fetches a page and its subresources into a hidden
WebContentswithout 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 anid()and shareable acrossWebContentsthat belong to the same logical session. - `WebContents::CreateWithSessionStorage` vs `WebContents::Create`
- The former binds the new
WebContentsto a caller-supplied session-storage namespace map, while the latter gives the newWebContentsa fresh, independent namespace. - `Matches()`
- The
NoStatePrefetchContentspredicate 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.
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
- Trigger a prefetch
A page initiates a NoStatePrefetch for a target URL, causing
StartPrerenderingto create the hidden prefetchWebContents. - Namespace is shared
Because
CreateWebContents(session_storage_namespace)usedWebContents::CreateWithSessionStorage, the prefetch contents were bound to the launching tab’sSessionStorageNamespacerather than an isolated one. - 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). - 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
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.cccomponents/no_state_prefetch/browser/no_state_prefetch_contents.hcomponents/no_state_prefetch/browser/no_state_prefetch_contents.cc
Audit Directions
- Legacy swap-in assumptionsReview 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 namespacesAudit all callers of
WebContents::CreateWithSessionStorageandCreateMapWithDefaultSessionStorageNamespaceto confirm each recipientWebContentsgenuinely requires the shared namespace rather than an isolated one. - Matching vs. access conflationLook 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.
Patch
From fcd4f10edbdcee80d2f510d95b32cb5bfcc3abbb Mon Sep 17 00:00:00 2001 From: Test User <[email protected]> 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 <[email protected]> Reviewed-by: Hiroki Nakagawa <[email protected]> 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,
Regression Test / PoC
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,