Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in BFCache
DescriptionInappropriate implementation in BFCache
ComponentBFCache
Bug ClassLogic Error
Tracker417215501
Fix commitc287a87a87fd (chromium/src) +212/-57
CISA KEVNot listed
CreditedJesper van den Ende (Pelican Party Studios)
Disclosed2025-05-27

Changed Functions

FunctionChangeNotes
BackForwardCacheBrowserTestWithPrerendering
content/browser/back_forward_cache_no_store_browsertest.cc
modified
switch
content/browser/back_forward_cache_no_store_browsertest.cc
modified
BackForwardCacheBrowserTestAllowCacheControlNoStore
content/browser/back_forward_cache_no_store_browsertest.cc
modified

Files Changed

  • content/browser/back_forward_cache_no_store_browsertest.cc
From c287a87a87fd8b223af761746252ae03292024af Mon Sep 17 00:00:00 2001
From: Mingyu Lei <[email protected]>
Date: Mon, 19 May 2025 23:01:44 -0700
Subject: [PATCH] CCNS: make sure the cookie listener is added for prerendered document

Previously the cookie listen is not added for the prerendered document
because we only check IsInPrimaryMainFrame(), this will lead to serious
security problem since we won't be notified when the cookie changes
throughout the life time of that document.

This CL fixes the check and update the browser test with prerendering
case. Only a subset of the tests are updated but that should provide
enough coverage for the scenarios we are caring about.

Change-Id: Ia538f6f9e72c1096f1d0b4ed5ade7e2bd56e5523
Bug: 417215501
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6550761
Reviewed-by: Rakina Zata Amni <[email protected]>
Commit-Queue: Mingyu Lei <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1462580}
---

diff --git a/content/browser/back_forward_cache_no_store_browsertest.cc b/content/browser/back_forward_cache_no_store_browsertest.cc
index 9eec1c6e..2f482a6 100644
--- a/content/browser/back_forward_cache_no_store_browsertest.cc
+++ b/content/browser/back_forward_cache_no_store_browsertest.cc
@@ -14,10 +14,12 @@
 #include "content/browser/renderer_host/render_frame_host_impl.h"
 #include "content/browser/web_contents/web_contents_impl.h"
 #include "content/public/browser/content_browser_client.h"
+#include "content/public/browser/web_contents.h"
 #include "content/public/common/content_switches.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/content_browser_test_utils.h"
+#include "content/public/test/prerender_test_util.h"
 #include "content/public/test/test_navigation_observer.h"
 #include "content/shell/browser/shell.h"
 #include "net/base/features.h"
@@ -160,41 +162,111 @@
                     {}, {}, {}, FROM_HERE);
 }
 
-namespace {
+enum class TestNavigationType {
+  kNonPrerender,
+  kPrerender,
+};
+
+class BackForwardCacheBrowserTestWithPrerendering
+    : public BackForwardCacheBrowserTest,
+      public ::testing::WithParamInterface<TestNavigationType> {
+ public:
+  static std::string DescribeParams(
+      const ::testing::TestParamInfo<ParamType>& info) {
+    switch (info.param) {
+      case TestNavigationType::kNonPrerender:
+        return "NonPrerender";
+      case TestNavigationType::kPrerender:
+        return "Prerender";
+    }
+  }
+
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
+    // `prerender_helper_` has a ScopedFeatureList so we needed to delay its
+    // creation until the BackForwardCacheBrowserTest finishes setting up the
+    // ScopedFeatureList.
+    prerender_helper_ =
+        std::make_unique<test::PrerenderTestHelper>(base::BindRepeating(
+            &BackForwardCacheBrowserTestWithPrerendering::GetWebContents,
+            base::Unretained(this)));
+    prerender_helper_->RegisterServerRequestMonitor(embedded_test_server());
+  }
+
+  WebContents* GetWebContents() { return web_contents(); }
+
+  test::PrerenderTestHelper& prerender_helper() { return *prerender_helper_; }
+
+ protected:
+  void NavigateToPageWithResponseFromMainWebContents(
+      GURL& url,
+      net::test_server::ControllableHttpResponse& response,
+      const char* response_bytes) {
+    switch (GetParam()) {
+      case TestNavigationType::kNonPrerender: {
+        TestNavigationObserver observer(web_contents());
+        shell()->LoadURL(url);
+        response.WaitForRequest();
+        response.Send(response_bytes);
+        response.Done();
+        observer.Wait();
+        break;
+      }
+      case TestNavigationType::kPrerender: {
+        prerender_helper().AddPrerenderAsync(url);
+        response.WaitForRequest();
+        response.Send(response_bytes);
+        response.Done();
+        TestActivationManager activation_manager(web_contents(), url);
+        ASSERT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
+                           JsReplace("location = $1", url)));
+        activation_manager.WaitForNavigationFinished();
+        EXPECT_TRUE(activation_manager.was_activated());
+        break;
+      }
+    }
+  }
+
+ private:
+  std::unique_ptr<test::PrerenderTestHelper> prerender_helper_;
+};
 
 class BackForwardCacheBrowserTestAllowCacheControlNoStore
-    : public BackForwardCacheBrowserTest {
+    : public BackForwardCacheBrowserTestWithPrerendering {
  protected:
   void SetUpCommandLine(base::CommandLine* command_line) override {
     EnableFeatureAndSetParams(features::kBackForwardCache, "", "");
     EnableFeatureAndSetParams(
         features::kCacheControlNoStoreEnterBackForwardCache, "level",
         "store-and-evict");
-    BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
+    BackForwardCacheBrowserTestWithPrerendering::SetUpCommandLine(command_line);
   }
 };
 
-}  // namespace
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    BackForwardCacheBrowserTestAllowCacheControlNoStore,
+    ::testing::Values(TestNavigationType::kNonPrerender,
+                      TestNavigationType::kPrerender),
+    &BackForwardCacheBrowserTestAllowCacheControlNoStore::DescribeParams);
 
 // Test that a page with cache-control:no-store enters bfcache with the flag on,
 // but does not get restored and gets evicted.
-IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestAllowCacheControlNoStore,
+IN_PROC_BROWSER_TEST_P(BackForwardCacheBrowserTestAllowCacheControlNoStore,
                        PagesWithCacheControlNoStoreEnterBfcacheAndEvicted) {
   net::test_server::ControllableHttpResponse response(embedded_test_server(),
                                                       "/title1.html");
   ASSERT_TRUE(embedded_test_server()->Start());
+  GURL url_initial(embedded_test_server()->GetURL("a.com", "/title3.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), url_initial));
 
   GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
   GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html"));
 
   // 1) Load the document and specify no-store for the main resource.
-  TestNavigationObserver observer(web_contents());
-  shell()->LoadURL(url_a);
+  NavigateToPageWithResponseFromMainWebContents(url_a, response,
+                                                kResponseWithNoCache);
   RenderFrameHostImplWrapper rfh_a(current_frame_host());
-  response.WaitForRequest();
-  response.Send(kResponseWithNoCache);
-  response.Done();
-  observer.Wait();
   rfh_a->GetBackForwardCacheMetrics()->SetObserverForTesting(this);
 
   // 2) Navigate away. |rfh_a| should enter the bfcache.
@@ -216,12 +288,14 @@
 // Test that a page with cache-control:no-store enters bfcache with the flag on,
 // and if a cookie is modified while it is in bfcache via JavaScript, gets
 // evicted with cookie modified marked.
-IN_PROC_BROWSER_TEST_F(
+IN_PROC_BROWSER_TEST_P(
     BackForwardCacheBrowserTestAllowCacheControlNoStore,
     PagesWithCacheControlNoStoreCookieModifiedThroughJavaScript) {
   net::test_server::ControllableHttpResponse response(embedded_test_server(),
                                                       "/title1.html");
   ASSERT_TRUE(embedded_test_server()->Start());
+  GURL url_initial(embedded_test_server()->GetURL("a.com", "/title3.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), url_initial));
 
   GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
   GURL url_a_2(embedded_test_server()->GetURL("a.com", "/title2.html"));
@@ -231,13 +305,9 @@
   Shell* tab_to_modify_cookie = CreateBrowser();
 
   // 1) Load the document and specify no-store for the main resource.
-  TestNavigationObserver observer(tab_to_be_bfcached->web_contents());
-  tab_to_be_bfcached->LoadURL(url_a);
+  NavigateToPageWithResponseFromMainWebContents(url_a, response,
+                                                kResponseWithNoCache);
   RenderFrameHostImplWrapper rfh_a(current_frame_host());
-  response.WaitForRequest();
-  response.Send(kResponseWithNoCache);
-  response.Done();
-  observer.Wait();
   rfh_a->GetBackForwardCacheMetrics()->SetObserverForTesting(this);
 
   // 2) Set a normal cookie from JavaScript.
@@ -269,24 +339,89 @@
                   BlockListedFeatures()));
 }
 
+// Test that a prerendered page with cache-control:no-store enters bfcache with
+// the flag on, and if a cookie is modified before the prerendered page is
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/back_forward_cache_no_store_browsertest.cc b/content/browser/back_forward_cache_no_store_browsertest.cc
index 9eec1c6e..2f482a6 100644
--- a/content/browser/back_forward_cache_no_store_browsertest.cc
+++ b/content/browser/back_forward_cache_no_store_browsertest.cc
@@ -14,10 +14,12 @@
 #include "content/browser/renderer_host/render_frame_host_impl.h"
 #include "content/browser/web_contents/web_contents_impl.h"
 #include "content/public/browser/content_browser_client.h"
+#include "content/public/browser/web_contents.h"
 #include "content/public/common/content_switches.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/content_browser_test_utils.h"
+#include "content/public/test/prerender_test_util.h"
 #include "content/public/test/test_navigation_observer.h"
 #include "content/shell/browser/shell.h"
 #include "net/base/features.h"
@@ -160,41 +162,111 @@
                     {}, {}, {}, FROM_HERE);
 }
 
-namespace {
+enum class TestNavigationType {
+  kNonPrerender,
+  kPrerender,
+};
+
+class BackForwardCacheBrowserTestWithPrerendering
+    : public BackForwardCacheBrowserTest,
+      public ::testing::WithParamInterface<TestNavigationType> {
+ public:
+  static std::string DescribeParams(
+      const ::testing::TestParamInfo<ParamType>& info) {
+    switch (info.param) {
+      case TestNavigationType::kNonPrerender:
+        return "NonPrerender";
+      case TestNavigationType::kPrerender:
+        return "Prerender";
+    }
+  }
+
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
+    // `prerender_helper_` has a ScopedFeatureList so we needed to delay its
+    // creation until the BackForwardCacheBrowserTest finishes setting up the
+    // ScopedFeatureList.
+    prerender_helper_ =
+        std::make_unique<test::PrerenderTestHelper>(base::BindRepeating(
+            &BackForwardCacheBrowserTestWithPrerendering::GetWebContents,
+            base::Unretained(this)));
+    prerender_helper_->RegisterServerRequestMonitor(embedded_test_server());
+  }
+
+  WebContents* GetWebContents() { return web_contents(); }
+
+  test::PrerenderTestHelper& prerender_helper() { return *prerender_helper_; }
+
+ protected:
+  void NavigateToPageWithResponseFromMainWebContents(
+      GURL& url,
+      net::test_server::ControllableHttpResponse& response,
+      const char* response_bytes) {
+    switch (GetParam()) {
+      case TestNavigationType::kNonPrerender: {
+        TestNavigationObserver observer(web_contents());
+        shell()->LoadURL(url);
+        response.WaitForRequest();
+        response.Send(response_bytes);
+        response.Done();
+        observer.Wait();
+        break;
+      }
+      case TestNavigationType::kPrerender: {
+        prerender_helper().AddPrerenderAsync(url);
+        response.WaitForRequest();
+        response.Send(response_bytes);
+        response.Done();
+        TestActivationManager activation_manager(web_contents(), url);
+        ASSERT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
+                           JsReplace("location = $1", url)));
+        activation_manager.WaitForNavigationFinished();
+        EXPECT_TRUE(activation_manager.was_activated());
+        break;
+      }
+    }
+  }
+
+ private:
+  std::unique_ptr<test::PrerenderTestHelper> prerender_helper_;
+};
 
 class BackForwardCacheBrowserTestAllowCacheControlNoStore
-    : public BackForwardCacheBrowserTest {
+    : public BackForwardCacheBrowserTestWithPrerendering {
  protected:
   void SetUpCommandLine(base::CommandLine* command_line) override {
     EnableFeatureAndSetParams(features::kBackForwardCache, "", "");
     EnableFeatureAndSetParams(
         features::kCacheControlNoStoreEnterBackForwardCache, "level",
         "store-and-evict");
-    BackForwardCacheBrowserTest::SetUpCommandLine(command_line);
+    BackForwardCacheBrowserTestWithPrerendering::SetUpCommandLine(command_line);
   }
 };
 
-}  // namespace
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    BackForwardCacheBrowserTestAllowCacheControlNoStore,
+    ::testing::Values(TestNavigationType::kNonPrerender,
+                      TestNavigationType::kPrerender),
+    &BackForwardCacheBrowserTestAllowCacheControlNoStore::DescribeParams);
 
 // Test that a page with cache-control:no-store enters bfcache with the flag on,
 // but does not get restored and gets evicted.
-IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestAllowCacheControlNoStore,
+IN_PROC_BROWSER_TEST_P(BackForwardCacheBrowserTestAllowCacheControlNoStore,
                        PagesWithCacheControlNoStoreEnterBfcacheAndEvicted) {
   net::test_server::ControllableHttpResponse response(embedded_test_server(),
                                                       "/title1.html");
   ASSERT_TRUE(embedded_test_server()->Start());
+  GURL url_initial(embedded_test_server()->GetURL("a.com", "/title3.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), url_initial));
 
   GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
   GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html"));
 
   // 1) Load the document and specify no-store for the main resource.
-  TestNavigationObserver observer(web_contents());
-  shell()->LoadURL(url_a);
+  NavigateToPageWithResponseFromMainWebContents(url_a, response,
+                                                kResponseWithNoCache);
   RenderFrameHostImplWrapper rfh_a(current_frame_host());
-  response.WaitForRequest();
-  response.Send(kResponseWithNoCache);
-  response.Done();
-  observer.Wait();
   rfh_a->GetBackForwardCacheMetrics()->SetObserverForTesting(this);
 
   // 2) Navigate away. |rfh_a| should enter the bfcache.
@@ -216,12 +288,14 @@
 // Test that a page with cache-control:no-store enters bfcache with the flag on,
 // and if a cookie is modified while it is in bfcache via JavaScript, gets
 // evicted with cookie modified marked.
-IN_PROC_BROWSER_TEST_F(
+IN_PROC_BROWSER_TEST_P(
     BackForwardCacheBrowserTestAllowCacheControlNoStore,
     PagesWithCacheControlNoStoreCookieModifiedThroughJavaScript) {
   net::test_server::ControllableHttpResponse response(embedded_test_server(),
                                                       "/title1.html");
   ASSERT_TRUE(embedded_test_server()->Start());
+  GURL url_initial(embedded_test_server()->GetURL("a.com", "/title3.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), url_initial));
 
   GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
   GURL url_a_2(embedded_test_server()->GetURL("a.com", "/title2.html"));
@@ -231,13 +305,9 @@
   Shell* tab_to_modify_cookie = CreateBrowser();
 
   // 1) Load the document and specify no-store for the main resource.
-  TestNavigationObserver observer(tab_to_be_bfcached->web_contents());
-  tab_to_be_bfcached->LoadURL(url_a);
+  NavigateToPageWithResponseFromMainWebContents(url_a, response,
+                                                kResponseWithNoCache);
   RenderFrameHostImplWrapper rfh_a(current_frame_host());
-  response.WaitForRequest();
-  response.Send(kResponseWithNoCache);
-  response.Done();
-  observer.Wait();
   rfh_a->GetBackForwardCacheMetrics()->SetObserverForTesting(this);
 
   // 2) Set a normal cookie from JavaScript.
@@ -269,24 +339,89 @@
                   BlockListedFeatures()));
 }
 
+// Test that a prerendered page with cache-control:no-store enters bfcache with
+// the flag on, and if a cookie is modified before the prerendered page is
+// activated via JavaScript, gets evicted with cookie modified marked.
+IN_PROC_BROWSER_TEST_F(
+    BackForwardCacheBrowserTestAllowCacheControlNoStore,
+    PagesWithCacheControlNoStoreCookieModifiedBeforePrerendererActivationThroughJavaScript) {
+  net::test_server::ControllableHttpResponse response(embedded_test_server(),
+                                                      "/title1.html");
+  ASSERT_TRUE(embedded_test_server()->Start());
+  GURL url_initial(embedded_test_server()->GetURL("a.com", "/title3.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), url_initial));
+
+  GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  GURL url_a_2(embedded_test_server()->GetURL("a.com", "/title2.html"));
+  GURL url_b(embedded_test_server()->GetURL("b.com", "/title3.html"));
+
+  Shell* tab_to_be_bfcached = shell();
+  Shell* tab_to_modify_cookie = CreateBrowser();
+
+  // 1) Prerender the document and specify no-store for the main resource.
+  prerender_helper().AddPrerenderAsync(url_a);
+  response.WaitForRequest();
+  response.Send(kResponseWithNoCache);
+  response.Done();
+  TestActivationManager activation_manager(web_contents(), url_a);
+  ASSERT_TRUE(ExecJs(web_contents()->GetPrimaryMainFrame(),
+                     JsReplace("location = $1", url_a)));
+  ASSERT_TRUE(activation_manager.WaitForAfterChecks());
+
+  // 2) Navigate to a.com in |tab_to_modify_cookie| and modify cookie from
+  // JavaScript before the page is activated.
+  EXPECT_TRUE(NavigateToURL(tab_to_modify_cookie, url_a_2));
+  EXPECT_TRUE(ExecJs(tab_to_modify_cookie, "document.cookie='foo=baz'"));
+  EXPECT_EQ("foo=baz", EvalJs(tab_to_modify_cookie, "document.cookie"));
+
+  // 3) Resume the activation.
+  activation_manager.WaitForNavigationFinished();
+  EXPECT_TRUE(activation_manager.was_activated());
+  RenderFrameHostImplWrapper rfh_a(current_frame_host());
+  rfh_a->GetBackForwardCacheMetrics()->SetObserverForTesting(this);
+
+  // 4) Navigate away. |rfh_a| should enter bfcache.
+  EXPECT_TRUE(NavigateToURL(tab_to_be_bfcached, url_b));
+  EXPECT_TRUE(rfh_a->IsInBackForwardCache());
+
+  // 5) Go back. |rfh_a| should be evicted upon restoration.
+  ASSERT_TRUE(HistoryGoBack(tab_to_be_bfcached->web_contents()));
+
+  EXPECT_EQ("foo=baz", EvalJs(tab_to_be_bfcached, "document.cookie"));
+  ExpectNotRestored({NotRestoredReason::kCacheControlNoStoreCookieModified}, {},
+                    {}, {}, {}, FROM_HERE);
+  // Make sure that the tree result also has the same reason.
+  EXPECT_THAT(GetTreeResult()->GetDocumentResult(),
+              MatchesDocumentResult(
+                  NotRestoredReasons(
+                      {NotRestoredReason::kCacheControlNoStoreCookieModified}),
+                  BlockListedFeatures()));
+}
+
 // Test that a page with cache-control:no-store enters bfcache with the flag on,
 // and if a cookie is modified, it gets evicted with cookie changed, but if
 // navigated away again and navigated back, it gets evicted without cookie
 // change marked.
-IN_PROC_BROWSER_TEST_F(BackForwardCacheBrowserTestAllowCacheControlNoStore,
+IN_PROC_BROWSER_TEST_P(BackForwardCacheBrowserTestAllowCacheControlNoStore,
                        PagesWithCacheControlNoStoreCookieModifiedBackTwice) {
+  net::test_server::ControllableHttpResponse response(embedded_test_server(),
+                                                      "/title1.html");
+  net::test_server::ControllableHttpResponse response_back(
+      embedded_test_server(), "/title1.html");
   ASSERT_TRUE(embedded_test_server()->Start());
+  GURL url_initial(embedded_test_server()->GetURL("a.com", "/title3.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), url_initial));
 
-  GURL url_a(embedded_test_server()->GetURL(
-      "a.com", "/set-header?Cache-Control: no-store"));
-  GURL url_a_2(embedded_test_server()->GetURL("a.com", "/title1.html"));
-  GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
+  GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  GURL url_a_2(embedded_test_server()->GetURL("a.com", "/title2.html"));
+  GURL url_b(embedded_test_server()->GetURL("b.com", "/title2.html"));
 
   Shell* tab_to_be_bfcached = shell();
   Shell* tab_to_modify_cookie = CreateBrowser();
 
   // 1) Load the document and specify no-store for the main resource.
-  EXPECT_TRUE(NavigateToURL(tab_to_be_bfcached, url_a));
+  NavigateToPageWithResponseFromMainWebContents(url_a, response,
+                                                kResponseWithNoCache);
   RenderFrameHostImplWrapper rfh_a(current_frame_host());
   rfh_a->GetBackForwardCacheMetrics()->SetObserverForTesting(this);
 
@@ -306,7 +441,12 @@
   EXPECT_EQ("foo=baz", EvalJs(tab_to_modify_cookie, "document.cookie"));
 
   // 5) Go back. |rfh_a| should be evicted upon restoration.
-  ASSERT_TRUE(HistoryGoBack(tab_to_be_bfcached->web_contents()));
+  TestNavigationObserver observer(tab_to_be_bfcached->web_contents());
+  tab_to_be_bfcached->web_contents()->GetController().GoBack();
+  response_back.WaitForRequest();
+  response_back.Send(kResponseWithNoCache);
+  response_back.Done();
+  observer.Wait();
 
   EXPECT_EQ("foo=baz", EvalJs(tab_to_be_bfcached, "document.cookie"));
   ExpectNotRestored({NotRestoredReason::kCacheControlNoStoreCookieModified}, {},
@@ -337,12 +477,14 @@
 // Test that a page with cache-control:no-store enters bfcache with the flag on,
 // and even if a cookie is modified on a different domain than the entry, the
 // entry is not marked as cookie modified.
-IN_PROC_BROWSER_TEST_F(
+IN_PROC_BROWSER_TEST_P(
     BackForwardCacheBrowserTestAllowCacheControlNoStore,
     PagesWithCacheControlNoStoreCookieModifiedThroughJavaScriptOnDifferentDomain) {
   net::test_server::ControllableHttpResponse response(embedded_test_server(),
                                                       "/title1.html");
   ASSERT_TRUE(embedded_test_server()->Start());
+  GURL url_initial(embedded_test_server()->GetURL("a.com", "/title3.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), url_initial));
 
   GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
   GURL url_a_2(embedded_test_server()->GetURL("a.com", "/title2.html"));
@@ -352,13 +494,9 @@
   Shell* tab_to_modify_cookie = CreateBrowser();
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

Speculation rules conflict with BFCache, causing potentially sensitive pages to be cached when they shouldn't

Security Bug

Important: Please do not change the component of this bug manually.

Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md

Please see the following link for instructions on filing security bugs: https://www.chromium.org/Home/chromium-security/reporting-security-bugs

Reports may be eligible for reward payments under the Chrome VRP: https://g.co/chrome/vrp

NOTE: Security bugs are normally made public once a fix has been widely deployed.


VULNERABILITY DETAILS

Normally bfcache is cleared on pages with a Cache-Control: no-cache header when cookies change. See https://developer.chrome.com/docs/web-platform/bfcache-ccns for details. However, it seems like the Speculation Rules Api conflicts with this mechanism, causing pages to remain cached after logging out from a site for example.

VERSION

Chrome Version: 123.0.6300.0 (Developer Build) (arm64)

Operating System: macOS 15.4.1 (24E263)

A bisect gives the following info:

You are probably looking for a change made after 1260232 (known good), but no later than 1260262 (first known bad).

CHANGELOG URL: https://chromium.googlesource.com/chromium/src/+log/57c9ed1ccc03628d9088996ad57899deec1a4aee..29c4db2dad1378977a2334ffd62e0a8876677ace

And this commit seems to be the most likely culprit, it is the only one touching bfcache as far as I can tell: https://chromium.googlesource.com/chromium/src/+/9ec5a4888be7a555a95aeb7d88cc7bb603a9f0ca

REPRODUCTION CASE

First let’s check the expected behavior

  1. Visit https://bfcache-speculation-rules.deno.dev/
  2. Click the Login button
  3. Click Home
  4. Click View the secret page
  5. Click Logout
  6. Click the browser back button

Observe how the page now shows ‘Access restricted’, as expected since the user has been logged out.

Now for the actual behavior with speculation rules enabled

  1. Visit https://bfcache-speculation-rules.deno.dev/
  2. This time, make sure speculation rules are enabled (click Toggle speculation rules)
  3. Click the Login button
  4. Click Home
  5. Click View the secret page
  6. Click Logout
  7. Click the browser back button

This time, the secret content is still visible, presumably due to bfcache showing the cached content. Only after refreshing the page will it show the ‘Access restricted’ message.

CREDIT INFORMATION

Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited?

Reporter credit: Jesper van den Ende - Pelican Party Studios

View on issue tracker