Medium watch ✔︎ verified CVSS 4.2 chrome Authorization Error 🔧 Commit mapped 🧬 Authorization check exists but is not enforced on every path

Overview

Medium
Severity
4.2
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Browser
DescriptionMissing authorization in Browser
ComponentBrowser
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety — factors set by independent review by AI
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.
Independent review by AIConfirmed
Reviewer notes: Core mechanism (no IsActive check in GetInterstitialForFrame :468-478) is confirmed by diff and BFCache test. Overstates GoBack impact. Requires supervised profile plus compromised renderer. Mapped fc1379d is a refactor; a4a13bb is the fix.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker523715133
Fix commita4a13bba63c4 (chromium/src) +188/-0
Related commitsfc1379decbca previously mapped (not the fix) · chromium/src
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-06-14 (per vendor advisory)
Fix landed2026-09-10 (public git)
Disclosed2026-09-15
Patch gap5 days — fix public in source before the advisory

Background

`SupervisedUserNavigationObserver`
the browser-process WebContentsObserver that stores supervised-user interstitials and implements the SupervisedUserCommands mojo interface.
`GetInterstitialForFrame`
helper that looks up the stored SupervisedUserInterstitial for the current mojo target frame by its FrameTreeNodeId.
Back/Forward Cache (BFCache)
a cache that keeps a navigated-away page’s RenderFrameHost alive but inactive so it can later be restored.

Root Cause Analysis

Re-mapped on 2026-10-03: our pipeline had picked the earliest commit citing this bug (fc1379decbca), which an audit found was a preparatory/helper change, not the fix. This page now points to the commit whose diff removes the bug.

The SupervisedUserCommands handlers — GoBack (supervised_user_navigation_observer.cc:480), RequestUrlAccessRemote (:494), RequestUrlAccessLocal (:508), and LearnMore (:520) — all resolve their target through GetInterstitialForFrame() at :468-478. Pre-fix, that helper reads receivers_.CurrentTargetFrame() and its GetFrameTreeNodeId() (:470-471) and looks the id up in supervised_user_interstitials_ (:473-476) with no check that the calling frame is still active. The interface’s own contract (header comment at .h:126-128) says these commands “should not be called when an interstitial is no longer showing” and that this “should be enforced by the mojo caller,” an invariant the handlers never verified themselves. Because the receiver set still delivers messages from a BFCached frame and the subframe’s interstitial entry is not cleared when the page enters BFCache (per the added browsertest), an inactive frame could still drive the URL-access commands (RequestUrlAccessRemote, RequestUrlAccessLocal) against the stored interstitial; GoBack was already limited to the primary main frame (:482), which a BFCached frame is not, and the commit message notes GoBack/LearnMore “don’t have any external interactions”.

The fix adds if (!target_frame.IsActive()) { return nullptr; } at the top of GetInterstitialForFrame(), so every handler now treats an inactive frame as having no interstitial — RequestUrlAccessRemote/RequestUrlAccessLocal run their callback with request_issued=false and the others return early.

The patch TIGHTENS authorization by adding the missing liveness check.

Key insight
The URL-access handlers (RequestUrlAccessRemote/RequestUrlAccessLocal) authorized callers solely by a FrameTreeNodeId map lookup and never checked frame liveness (only GoBack added an IsInPrimaryMainFrame() gate); adding an IsActive() gate in GetInterstitialForFrame() rejects requests from inactive/BFCached frames.

Attack Path

  1. Embed a blocked subframe A supervised user loads a page containing a subframe whose URL is blocked, creating a stored SupervisedUserInterstitial entry for that subframe.
  2. Push the page into BFCache The main frame navigates away, so the page and its blocked subframe become inactive but are retained in BFCache with the interstitial entry still in the map.
  3. Drive a command from the inactive frame A compromised renderer sends e.g. RequestUrlAccessRemote over the still-bound pipe from the BFCached frame. (frame dispatch per test; renderer compromise inferred)
  4. Request processed unauthorized Pre-fix, GetInterstitialForFrame returns the leaked interstitial and the URL-access command is issued on behalf of a page that is no longer active.

Impact Assessment

In the browser process, an inactive or BFCached frame could issue supervised-user URL-access requests (RequestUrlAccessRemote/RequestUrlAccessLocal) on behalf of an interstitial that is no longer showing; GoBack was already blocked for such frames by its IsInPrimaryMainFrame() check (:482), and the commit message says GoBack/LearnMore have no external interactions. Preconditions are a supervised profile and a compromised renderer (inferred). The advisory rates this medium severity, classifies it as missing authorization, and states it was not exploited in the wild.

Changed Functions

FunctionChangeNotes
GetInterstitialForFrame
chrome/browser/supervised_user/supervised_user_navigation_observer.cc
modified
receivers_for_test
chrome/browser/supervised_user/supervised_user_navigation_observer.h
modified
SupervisedUserBFCacheIframeTest
chrome/browser/supervised_user/supervised_user_navigation_throttle_browsertest.cc
modified
SupervisedUserBFCacheIframeTest
chrome/browser/supervised_user/supervised_user_navigation_throttle_browsertest.cc
modified

Files Changed

  • chrome/browser/supervised_user/supervised_user_navigation_observer.cc
  • chrome/browser/supervised_user/supervised_user_navigation_observer.h
  • chrome/browser/supervised_user/supervised_user_navigation_throttle_browsertest.cc

Audit Directions

  • Frame-id lookup without liveness
    Flag browser-process mojo handlers that resolve per-frame state by FrameTreeNodeId without first checking RenderFrameHost::IsActive().
  • BFCache message delivery
    Verify interface handlers either reject or safely tolerate messages that arrive from BFCached or otherwise inactive frames.
diff --git a/chrome/browser/supervised_user/supervised_user_navigation_observer.cc b/chrome/browser/supervised_user/supervised_user_navigation_observer.cc
index c60e00f..11ec8327 100644
--- a/chrome/browser/supervised_user/supervised_user_navigation_observer.cc
+++ b/chrome/browser/supervised_user/supervised_user_navigation_observer.cc
@@ -468,6 +468,11 @@
 supervised_user::SupervisedUserInterstitial*
 SupervisedUserNavigationObserver::GetInterstitialForFrame() {
   content::RenderFrameHost& target_frame = receivers_.CurrentTargetFrame();
+
+  if (!target_frame.IsActive()) {
+    return nullptr;
+  }
+
   content::FrameTreeNodeId frame_id = target_frame.GetFrameTreeNodeId();
 
   if (auto it = supervised_user_interstitials_.find(frame_id);
diff --git a/chrome/browser/supervised_user/supervised_user_navigation_observer.h b/chrome/browser/supervised_user/supervised_user_navigation_observer.h
index 4e87d0b..d15316eb 100644
--- a/chrome/browser/supervised_user/supervised_user_navigation_observer.h
+++ b/chrome/browser/supervised_user/supervised_user_navigation_observer.h
@@ -94,6 +94,12 @@
     return requested_hosts_;
   }
 
+  content::RenderFrameHostReceiverSet<
+      supervised_user::mojom::SupervisedUserCommands>&
+  receivers_for_test() {
+    return receivers_;
+  }
+
  private:
   friend class content::WebContentsUserData<SupervisedUserNavigationObserver>;
 
diff --git a/chrome/browser/supervised_user/supervised_user_navigation_throttle_browsertest.cc b/chrome/browser/supervised_user/supervised_user_navigation_throttle_browsertest.cc
index 4c3b37d..b21bfdf 100644
--- a/chrome/browser/supervised_user/supervised_user_navigation_throttle_browsertest.cc
+++ b/chrome/browser/supervised_user/supervised_user_navigation_throttle_browsertest.cc
@@ -43,11 +43,13 @@
 #include "content/public/browser/web_contents.h"
 #include "content/public/common/content_paths.h"
 #include "content/public/common/page_type.h"
+#include "content/public/test/back_forward_cache_util.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/fenced_frame_test_util.h"
 #include "content/public/test/prerender_test_util.h"
 #include "content/public/test/test_navigation_observer.h"
+#include "content/public/test/test_utils.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/base/page_transition_types.h"
@@ -1363,4 +1365,179 @@
   EXPECT_TRUE(rfh_host2);
 }
 
+struct SupervisedUserBFCacheIframeTestParam {
+  using RequestUrlAccessMethod =
+      void (supervised_user::mojom::SupervisedUserCommands::*)(
+          base::OnceCallback<void(bool)>);
+
+  std::string test_name;
+  // References either local or remote url access method, which have different
+  // function behavior but identical signatures and semantic meaning.
+  RequestUrlAccessMethod request_url_access_method;
+};
+
+// Security regression test: verifies that a blocked subframe that has been
+// placed into the Back/Forward Cache cannot drive RequestUrlAccessRemote /
+// RequestUrlAccessLocal on behalf of an unrelated, currently-active page, but
+// once restored from the Back/Forward Cache, the active frame can successfully
+// drive the request.
+class SupervisedUserBFCacheIframeTest
+    : public SupervisedUserIframeFilterTest,
+      public testing::WithParamInterface<SupervisedUserBFCacheIframeTestParam> {
+ protected:
+  SupervisedUserBFCacheIframeTest() {
+    bfcache_feature_list_.InitWithFeaturesAndParameters(
+        content::GetDefaultEnabledBackForwardCacheFeaturesForTesting(),
+        content::GetDefaultDisabledBackForwardCacheFeaturesForTesting());
+  }
+
+ private:
+  base::test::ScopedFeatureList bfcache_feature_list_;
+};
+
+// Tests if BFCached subframes are blocked from issuing approval requests unless
+// they are successfully restored.
+IN_PROC_BROWSER_TEST_P(SupervisedUserBFCacheIframeTest,
+                       BFCachedSubframeCantIssueApprovalRequest) {
+  // Step 1: block one iframe host, load an allowed top-level page that embeds
+  // it. The browser stores a SupervisedUserInterstitial for the subframe.
+  BlockHost(kIframeHost2);
+  GURL allowed_url_with_iframes = embedded_test_server()->GetURL(
+      kExampleHost, "/supervised_user/with_iframes.html");
+
+  kids_management_api_mock().AllowSubsequentClassifyUrl();
+  ASSERT_TRUE(
+      ui_test_utils::NavigateToURL(browser(), allowed_url_with_iframes));
+  ASSERT_FALSE(IsInterstitialBeingShownInMainFrame(browser()));
+
+  std::vector<content::FrameTreeNodeId> blocked = GetBlockedFrames();
+  ASSERT_EQ(blocked.size(), 1u);
+  content::FrameTreeNodeId blocked_frame_id = blocked[0];
+  content::RenderFrameHost* blocked_rfh = tracker()->GetHost(blocked_frame_id);
+  ASSERT_TRUE(blocked_rfh);
+  ASSERT_TRUE(blocked_rfh->IsActive());
+  content::RenderFrameHostWrapper blocked_rfh_wrapper(blocked_rfh);
+
+  // Step 2: navigate the main frame cross-origin so the original page (and
+  // its blocked subframe) becomes eligible for BFCache.
+  GURL second_page = embedded_test_server()->GetURL(
+      kExampleHost2, "/supervised_user/simple.html");
+  kids_management_api_mock().AllowSubsequentClassifyUrl();
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), second_page));
+  ASSERT_FALSE(IsInterstitialBeingShownInMainFrame(browser()));
+
+  // Step 3: confirm the page actually entered BFCache and the subframe RFH
+  // was *not* deleted. This validates that FrameDeleted() never fired for it.
+  ASSERT_FALSE(blocked_rfh_wrapper.IsDestroyed())
+      << "Subframe RFH was destroyed; page did not enter BFCache.";
+  ASSERT_EQ(blocked_rfh_wrapper->GetLifecycleState(),
+            content::RenderFrameHost::LifecycleState::kInBackForwardCache);
+  ASSERT_FALSE(blocked_rfh_wrapper->IsActive());
+
+  // Step 4: the interstitial entry for the BFCached subframe is still
+  // present. DidFinishNavigation() only clears the entry whose
+  // FrameTreeNodeId matches the navigating frame (the *main* frame here), so
+  // the subframe entry leaks.
+  ASSERT_TRUE(
+      supervised_user_navigation_observer()->interstitials_for_test().contains(
+          blocked_frame_id))
+      << "Interstitial map was cleared on BFCache entry; bug not reachable.";
+
+  // Step 5: simulate a compromised renderer sending RequestUrlAccessRemote()
+  // from the inactive (BFCached) frame. RenderFrameHostReceiverSet does not
+  // drop messages from BFCached RFHs, so a real renderer-controlled pipe
+  // could do exactly this. We use SetCurrentTargetFrameForTesting() to model
+  // that dispatch context without needing to actually run JS in a frozen
+  // renderer.
+  permission_creator()->SetPermissionResult(true);
+  supervised_user_navigation_observer()
+      ->receivers_for_test()
+      .SetCurrentTargetFrameForTesting(blocked_rfh_wrapper.get());
+
+  base::RunLoop run_loop;
+  bool request_issued = false;
+
+  supervised_user::mojom::SupervisedUserCommands* commands =
+      static_cast<supervised_user::mojom::SupervisedUserCommands*>(
+          supervised_user_navigation_observer());
+
+  (commands->*(GetParam().request_url_access_method))(base::BindOnce(
+      [](base::OnceClosure quit, bool* out, bool issued) {
+        *out = issued;
+        std::move(quit).Run();
+      },
+      run_loop.QuitClosure(), &request_issued));
+  run_loop.Run();
+
+  supervised_user_navigation_observer()
+      ->receivers_for_test()
+      .SetCurrentTargetFrameForTesting(nullptr);
+
+  // Request should have been rejected.
+  EXPECT_FALSE(request_issued)
+      << "Handler should have rejected the inactive-frame call.";
+  EXPECT_EQ(permission_creator()->url_requests().size(), 0u);
+  if (!permission_creator()->url_requests().empty()) {
+    EXPECT_EQ(permission_creator()->url_requests()[0].GetHost(), kIframeHost2);
+  }
+
+  // Step 6: Navigate back to the original page from BFCache.
+  ASSERT_TRUE(content::HistoryGoBack(web_contents()));
+
+  // The page and subframe are restored to active state.
+  ASSERT_FALSE(blocked_rfh_wrapper.IsDestroyed());
+  ASSERT_EQ(blocked_rfh_wrapper->GetLifecycleState(),
+            content::RenderFrameHost::LifecycleState::kActive);
+  ASSERT_TRUE(blocked_rfh_wrapper->IsActive());
+
+  // Step 7: Now that the frame is restored and active, the user can
+  // successfully re-trigger the URL approval request.
+  supervised_user_navigation_observer()
+      ->receivers_for_test()
+      .SetCurrentTargetFrameForTesting(blocked_rfh_wrapper.get());
+
+  base::RunLoop run_loop_after_restore;
+  bool request_issued_after_restore = false;
+
+  (commands->*(GetParam().request_url_access_method))(base::BindOnce(
+      [](base::OnceClosure quit, bool* out, bool issued) {
+        *out = issued;
+        std::move(quit).Run();
+      },
+      run_loop_after_restore.QuitClosure(), &request_issued_after_restore));
+  run_loop_after_restore.Run();
+
+  supervised_user_navigation_observer()
+      ->receivers_for_test()
+      .SetCurrentTargetFrameForTesting(nullptr);
+
+  EXPECT_TRUE(request_issued_after_restore)
+      << "Handler should have processed the request from the restored active "
+         "frame.";
+  if (GetParam().test_name == "RequestUrlAccessRemote") {
+    EXPECT_EQ(permission_creator()->url_requests().size(), 1u);
+    if (!permission_creator()->url_requests().empty()) {
+      EXPECT_EQ(permission_creator()->url_requests()[0].GetHost(),
+                kIframeHost2);
+    }
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    ,
+    SupervisedUserBFCacheIframeTest,
+    testing::Values(
+        SupervisedUserBFCacheIframeTestParam{
+            .test_name = "RequestUrlAccessRemote",
+            .request_url_access_method =
+                &supervised_user::mojom::SupervisedUserCommands::
+                    RequestUrlAccessRemote},
+        SupervisedUserBFCacheIframeTestParam{
+            .test_name = "RequestUrlAccessLocal",
+            .request_url_access_method =
+                &supervised_user::mojom::SupervisedUserCommands::
+                    RequestUrlAccessLocal}),
+    [](const testing::TestParamInfo<SupervisedUserBFCacheIframeTestParam>&
+           info) { return info.param.test_name; });
+
 }  // namespace
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/supervised_user/supervised_user_navigation_throttle_browsertest.cc b/chrome/browser/supervised_user/supervised_user_navigation_throttle_browsertest.cc
index 4c3b37d..b21bfdf 100644
--- a/chrome/browser/supervised_user/supervised_user_navigation_throttle_browsertest.cc
+++ b/chrome/browser/supervised_user/supervised_user_navigation_throttle_browsertest.cc
@@ -43,11 +43,13 @@
 #include "content/public/browser/web_contents.h"
 #include "content/public/common/content_paths.h"
 #include "content/public/common/page_type.h"
+#include "content/public/test/back_forward_cache_util.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/fenced_frame_test_util.h"
 #include "content/public/test/prerender_test_util.h"
 #include "content/public/test/test_navigation_observer.h"
+#include "content/public/test/test_utils.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/base/page_transition_types.h"
@@ -1363,4 +1365,179 @@
   EXPECT_TRUE(rfh_host2);
 }
 
+struct SupervisedUserBFCacheIframeTestParam {
+  using RequestUrlAccessMethod =
+      void (supervised_user::mojom::SupervisedUserCommands::*)(
+          base::OnceCallback<void(bool)>);
+
+  std::string test_name;
+  // References either local or remote url access method, which have different
+  // function behavior but identical signatures and semantic meaning.
+  RequestUrlAccessMethod request_url_access_method;
+};
+
+// Security regression test: verifies that a blocked subframe that has been
+// placed into the Back/Forward Cache cannot drive RequestUrlAccessRemote /
+// RequestUrlAccessLocal on behalf of an unrelated, currently-active page, but
+// once restored from the Back/Forward Cache, the active frame can successfully
+// drive the request.
+class SupervisedUserBFCacheIframeTest
+    : public SupervisedUserIframeFilterTest,
+      public testing::WithParamInterface<SupervisedUserBFCacheIframeTestParam> {
+ protected:
+  SupervisedUserBFCacheIframeTest() {
+    bfcache_feature_list_.InitWithFeaturesAndParameters(
+        content::GetDefaultEnabledBackForwardCacheFeaturesForTesting(),
+        content::GetDefaultDisabledBackForwardCacheFeaturesForTesting());
+  }
+
+ private:
+  base::test::ScopedFeatureList bfcache_feature_list_;
+};
+
+// Tests if BFCached subframes are blocked from issuing approval requests unless
+// they are successfully restored.
+IN_PROC_BROWSER_TEST_P(SupervisedUserBFCacheIframeTest,
+                       BFCachedSubframeCantIssueApprovalRequest) {
+  // Step 1: block one iframe host, load an allowed top-level page that embeds
+  // it. The browser stores a SupervisedUserInterstitial for the subframe.
+  BlockHost(kIframeHost2);
+  GURL allowed_url_with_iframes = embedded_test_server()->GetURL(
+      kExampleHost, "/supervised_user/with_iframes.html");
+
+  kids_management_api_mock().AllowSubsequentClassifyUrl();
+  ASSERT_TRUE(
+      ui_test_utils::NavigateToURL(browser(), allowed_url_with_iframes));
+  ASSERT_FALSE(IsInterstitialBeingShownInMainFrame(browser()));
+
+  std::vector<content::FrameTreeNodeId> blocked = GetBlockedFrames();
+  ASSERT_EQ(blocked.size(), 1u);
+  content::FrameTreeNodeId blocked_frame_id = blocked[0];
+  content::RenderFrameHost* blocked_rfh = tracker()->GetHost(blocked_frame_id);
+  ASSERT_TRUE(blocked_rfh);
+  ASSERT_TRUE(blocked_rfh->IsActive());
+  content::RenderFrameHostWrapper blocked_rfh_wrapper(blocked_rfh);
+
+  // Step 2: navigate the main frame cross-origin so the original page (and
+  // its blocked subframe) becomes eligible for BFCache.
+  GURL second_page = embedded_test_server()->GetURL(
+      kExampleHost2, "/supervised_user/simple.html");
+  kids_management_api_mock().AllowSubsequentClassifyUrl();
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), second_page));
+  ASSERT_FALSE(IsInterstitialBeingShownInMainFrame(browser()));
+
+  // Step 3: confirm the page actually entered BFCache and the subframe RFH
+  // was *not* deleted. This validates that FrameDeleted() never fired for it.
+  ASSERT_FALSE(blocked_rfh_wrapper.IsDestroyed())
+      << "Subframe RFH was destroyed; page did not enter BFCache.";
+  ASSERT_EQ(blocked_rfh_wrapper->GetLifecycleState(),
+            content::RenderFrameHost::LifecycleState::kInBackForwardCache);
+  ASSERT_FALSE(blocked_rfh_wrapper->IsActive());
+
+  // Step 4: the interstitial entry for the BFCached subframe is still
+  // present. DidFinishNavigation() only clears the entry whose
+  // FrameTreeNodeId matches the navigating frame (the *main* frame here), so
+  // the subframe entry leaks.
+  ASSERT_TRUE(
+      supervised_user_navigation_observer()->interstitials_for_test().contains(
+          blocked_frame_id))
+      << "Interstitial map was cleared on BFCache entry; bug not reachable.";
+
+  // Step 5: simulate a compromised renderer sending RequestUrlAccessRemote()
+  // from the inactive (BFCached) frame. RenderFrameHostReceiverSet does not
+  // drop messages from BFCached RFHs, so a real renderer-controlled pipe
+  // could do exactly this. We use SetCurrentTargetFrameForTesting() to model
+  // that dispatch context without needing to actually run JS in a frozen
+  // renderer.
+  permission_creator()->SetPermissionResult(true);
+  supervised_user_navigation_observer()
+      ->receivers_for_test()
+      .SetCurrentTargetFrameForTesting(blocked_rfh_wrapper.get());
+
+  base::RunLoop run_loop;
+  bool request_issued = false;
+
+  supervised_user::mojom::SupervisedUserCommands* commands =
+      static_cast<supervised_user::mojom::SupervisedUserCommands*>(
+          supervised_user_navigation_observer());
+
+  (commands->*(GetParam().request_url_access_method))(base::BindOnce(
+      [](base::OnceClosure quit, bool* out, bool issued) {
+        *out = issued;
+        std::move(quit).Run();
+      },
+      run_loop.QuitClosure(), &request_issued));
+  run_loop.Run();
+
+  supervised_user_navigation_observer()
+      ->receivers_for_test()
+      .SetCurrentTargetFrameForTesting(nullptr);
+
+  // Request should have been rejected.
+  EXPECT_FALSE(request_issued)
+      << "Handler should have rejected the inactive-frame call.";
+  EXPECT_EQ(permission_creator()->url_requests().size(), 0u);
+  if (!permission_creator()->url_requests().empty()) {
+    EXPECT_EQ(permission_creator()->url_requests()[0].GetHost(), kIframeHost2);
+  }
+
+  // Step 6: Navigate back to the original page from BFCache.
+  ASSERT_TRUE(content::HistoryGoBack(web_contents()));
+
+  // The page and subframe are restored to active state.
+  ASSERT_FALSE(blocked_rfh_wrapper.IsDestroyed());
+  ASSERT_EQ(blocked_rfh_wrapper->GetLifecycleState(),
+            content::RenderFrameHost::LifecycleState::kActive);
+  ASSERT_TRUE(blocked_rfh_wrapper->IsActive());
+
+  // Step 7: Now that the frame is restored and active, the user can
+  // successfully re-trigger the URL approval request.
+  supervised_user_navigation_observer()
+      ->receivers_for_test()
+      .SetCurrentTargetFrameForTesting(blocked_rfh_wrapper.get());
+
+  base::RunLoop run_loop_after_restore;
+  bool request_issued_after_restore = false;
+
+  (commands->*(GetParam().request_url_access_method))(base::BindOnce(
+      [](base::OnceClosure quit, bool* out, bool issued) {
+        *out = issued;
+        std::move(quit).Run();
+      },
+      run_loop_after_restore.QuitClosure(), &request_issued_after_restore));
+  run_loop_after_restore.Run();
+
+  supervised_user_navigation_observer()
+      ->receivers_for_test()
+      .SetCurrentTargetFrameForTesting(nullptr);
+
+  EXPECT_TRUE(request_issued_after_restore)
+      << "Handler should have processed the request from the restored active "
+         "frame.";
+  if (GetParam().test_name == "RequestUrlAccessRemote") {
+    EXPECT_EQ(permission_creator()->url_requests().size(), 1u);
+    if (!permission_creator()->url_requests().empty()) {
+      EXPECT_EQ(permission_creator()->url_requests()[0].GetHost(),
+                kIframeHost2);
+    }
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    ,
+    SupervisedUserBFCacheIframeTest,
+    testing::Values(
+        SupervisedUserBFCacheIframeTestParam{
+            .test_name = "RequestUrlAccessRemote",
+            .request_url_access_method =
+                &supervised_user::mojom::SupervisedUserCommands::
+                    RequestUrlAccessRemote},
+        SupervisedUserBFCacheIframeTestParam{
+            .test_name = "RequestUrlAccessLocal",
+            .request_url_access_method =
+                &supervised_user::mojom::SupervisedUserCommands::
+                    RequestUrlAccessLocal}),
+    [](const testing::TestParamInfo<SupervisedUserBFCacheIframeTestParam>&
+           info) { return info.param.test_name; });
+
 }  // namespace
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.