CVE-2026-11187
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
WillStartOrRedirectRequestchrome/browser/actor/actor_navigation_throttle.cc |
modified | |
ifchrome/browser/actor/actor_navigation_throttle.cc |
modified | |
IN_PROC_BROWSER_TEST_Fchrome/browser/actor/execution_engine_browsertest.cc |
modified |
Files Changed
chrome/browser/actor/actor_navigation_throttle.ccchrome/browser/actor/execution_engine_browsertest.cc
Patch
From e2d4eea1931fd455390cf83f5a4954faf3f79021 Mon Sep 17 00:00:00 2001 From: Chris Fredrickson <[email protected]> Date: Wed, 29 Apr 2026 08:49:33 -0700 Subject: [PATCH] Fix safety-check bypass via history.back() Fixed: 502819675 Change-Id: I68cd499799835dee07b6cc25a180dd8682bdf962 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7800857 Reviewed-by: Dave Tapuska <[email protected]> Auto-Submit: Chris Fredrickson <[email protected]> Commit-Queue: Chris Fredrickson <[email protected]> Cr-Commit-Position: refs/heads/main@{#1622483} --- diff --git a/chrome/browser/actor/actor_navigation_throttle.cc b/chrome/browser/actor/actor_navigation_throttle.cc index 0a83a849..38d44a6 100644 --- a/chrome/browser/actor/actor_navigation_throttle.cc +++ b/chrome/browser/actor/actor_navigation_throttle.cc @@ -189,12 +189,10 @@ content::NavigationThrottle::ThrottleCheckResult ActorNavigationThrottle::WillStartOrRedirectRequest(bool is_redirection) { const GURL& navigation_url = navigation_handle()->GetURL(); - const std::optional<url::Origin>& initiator_origin = - navigation_handle()->GetInitiatorOrigin(); AggregatedJournal& journal = GetJournal(); - if (!is_redirection && !initiator_origin) { + if (!is_redirection && !navigation_handle()->IsRendererInitiated()) { journal.Log(navigation_url, task_id_, "NavThrottle", JournalDetailsBuilder() .Add("navigate", "Not triggered by page") diff --git a/chrome/browser/actor/execution_engine_browsertest.cc b/chrome/browser/actor/execution_engine_browsertest.cc index ac4be92a..a8d4454 100644 --- a/chrome/browser/actor/execution_engine_browsertest.cc +++ b/chrome/browser/actor/execution_engine_browsertest.cc @@ -59,14 +59,17 @@ #include "components/optimization_guide/content/browser/page_content_proto_provider.h" #include "components/optimization_guide/core/filters/optimization_hints_component_update_listener.h" #include "components/optimization_guide/proto/features/actions_data.pb.h" +#include "components/safe_browsing/core/common/safe_browsing_prefs.h" #include "components/viz/common/frame_sinks/copy_output_result.h" #include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_widget_host_view.h" #include "content/public/browser/web_contents.h" #include "content/public/common/content_client.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/download_test_observer.h" +#include "content/public/test/navigation_handle_observer.h" #include "content/public/test/prerender_test_util.h" #include "content/public/test/test_frame_navigation_observer.h" #include "content/public/test/test_navigation_observer.h" @@ -469,6 +472,47 @@ EXPECT_FALSE(browser_client().external_protocol_result().value()); } +// Regression test for https://crbug.com/502819675. +IN_PROC_BROWSER_TEST_F(ExecutionEngineBrowserTest, HistoryBackIsChecked) { + // Disable SafeBrowsing so that MayActOnUrl rejects every non-localhost URL + // with kSafeBrowsing. + safe_browsing::SetSafeBrowsingState( + browser()->profile()->GetPrefs(), + safe_browsing::SafeBrowsingState::NO_SAFE_BROWSING); + + // Disable BFCache so that `history.back()` is a real navigation. + content::DisableBackForwardCacheForTesting( + web_contents(), content::BackForwardCache::TEST_REQUIRES_NO_CACHING); + + const GURL first_url = + embedded_https_test_server().GetURL("a.com", "/empty.html"); + const GURL second_url = + embedded_https_test_server().GetURL("b.com", "/empty.html"); + + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), first_url)); + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), second_url)); + + actor_task().AddTab(active_tab()->GetHandle(), + /*stop_task_on_detach=*/true, base::DoNothing()); + ASSERT_TRUE(actor_task().IsActingOnTab(active_tab()->GetHandle())); + + // A `history.back()` navigation should be classified as renderer-initiated + // (even though the initiator is std::nullopt), and should be blocked by + // MayActOnUrl. + content::NavigationHandleObserver navigation_handle_observer(web_contents(), + first_url); + content::TestNavigationManager test_navigation_manager(web_contents(), + first_url); + EXPECT_TRUE(content::ExecJs(web_contents(), "history.back();", + content::EXECUTE_SCRIPT_NO_USER_GESTURE)); + ASSERT_TRUE(test_navigation_manager.WaitForNavigationFinished()); + ASSERT_TRUE(navigation_handle_observer.is_renderer_initiated()); + ASSERT_EQ(navigation_handle_observer.last_initiator_origin(), std::nullopt); + + EXPECT_FALSE(navigation_handle_observer.has_committed()); + EXPECT_EQ(second_url, web_contents()->GetLastCommittedURL()); +} + // TODO(crbug.com/456759397): Add coverage for multi-tab cases in // foreground/background visibility metric.
Regression Test / PoC
diff --git a/chrome/browser/actor/execution_engine_browsertest.cc b/chrome/browser/actor/execution_engine_browsertest.cc
index ac4be92a..a8d4454 100644
--- a/chrome/browser/actor/execution_engine_browsertest.cc
+++ b/chrome/browser/actor/execution_engine_browsertest.cc
@@ -59,14 +59,17 @@
#include "components/optimization_guide/content/browser/page_content_proto_provider.h"
#include "components/optimization_guide/core/filters/optimization_hints_component_update_listener.h"
#include "components/optimization_guide/proto/features/actions_data.pb.h"
+#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "components/viz/common/frame_sinks/copy_output_result.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.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/download_test_observer.h"
+#include "content/public/test/navigation_handle_observer.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
@@ -469,6 +472,47 @@
EXPECT_FALSE(browser_client().external_protocol_result().value());
}
+// Regression test for https://crbug.com/502819675.
+IN_PROC_BROWSER_TEST_F(ExecutionEngineBrowserTest, HistoryBackIsChecked) {
+ // Disable SafeBrowsing so that MayActOnUrl rejects every non-localhost URL
+ // with kSafeBrowsing.
+ safe_browsing::SetSafeBrowsingState(
+ browser()->profile()->GetPrefs(),
+ safe_browsing::SafeBrowsingState::NO_SAFE_BROWSING);
+
+ // Disable BFCache so that `history.back()` is a real navigation.
+ content::DisableBackForwardCacheForTesting(
+ web_contents(), content::BackForwardCache::TEST_REQUIRES_NO_CACHING);
+
+ const GURL first_url =
+ embedded_https_test_server().GetURL("a.com", "/empty.html");
+ const GURL second_url =
+ embedded_https_test_server().GetURL("b.com", "/empty.html");
+
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), first_url));
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), second_url));
+
+ actor_task().AddTab(active_tab()->GetHandle(),
+ /*stop_task_on_detach=*/true, base::DoNothing());
+ ASSERT_TRUE(actor_task().IsActingOnTab(active_tab()->GetHandle()));
+
+ // A `history.back()` navigation should be classified as renderer-initiated
+ // (even though the initiator is std::nullopt), and should be blocked by
+ // MayActOnUrl.
+ content::NavigationHandleObserver navigation_handle_observer(web_contents(),
+ first_url);
+ content::TestNavigationManager test_navigation_manager(web_contents(),
+ first_url);
+ EXPECT_TRUE(content::ExecJs(web_contents(), "history.back();",
+ content::EXECUTE_SCRIPT_NO_USER_GESTURE));
+ ASSERT_TRUE(test_navigation_manager.WaitForNavigationFinished());
+ ASSERT_TRUE(navigation_handle_observer.is_renderer_initiated());
+ ASSERT_EQ(navigation_handle_observer.last_initiator_origin(), std::nullopt);
+
+ EXPECT_FALSE(navigation_handle_observer.has_committed());
+ EXPECT_EQ(second_url, web_contents()->GetLastCommittedURL());
+}
+
// TODO(crbug.com/456759397): Add coverage for multi-tab cases in
// foreground/background visibility metric.
Original Bug Report
Potential ActorNavigationThrottle bypass via renderer-initiated history navigation
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 without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.
Overview: The ActorNavigationThrottle incorrectly relies on a null initiator_origin to identify browser-initiated navigations, bypassing critical safety checks. An attacker can exploit this using history.back() to navigate an active Glic AI task to a restricted chrome:// page. The AI subsequently observes the restricted page automatically and can be manipulated into exfiltrating its sensitive contents.
Affected files:
chrome/browser/actor/actor_navigation_throttle.ccchrome/browser/actor/site_policy.ccchrome/browser/actor/execution_engine.cc
Estimated timestamp from git blame: 2025-10-21
Description
There is a potential vulnerability in ActorNavigationThrottle::WillStartOrRedirectRequest (chrome/browser/actor/actor_navigation_throttle.cc) where navigation safety checks can be bypassed by renderer-initiated history navigations.
To enforce safety policies on navigations within tabs controlled by the Glic Actor, the throttle attempts to distinguish between page-triggered and browser-triggered navigations using the initiator_origin:
196: if (!is_redirection && !initiator_origin) {
197: journal.Log(navigation_url, task_id_, "NavThrottle",
198: JournalDetailsBuilder()
199: .Add("navigate", "Not triggered by page")
200: .Build());
201: return content::NavigationThrottle::PROCEED;
202: }
If the initiator_origin is nullopt, the throttle immediately returns PROCEED, skipping the critical actor::MayActOnUrl check at line 231. This check is responsible for blocking non-HTTP/HTTPS schemes, raw IP addresses, and enforcing Safe Browsing constraints.
However, per the content::NavigationHandle API contract, history navigations inherit the initiator_origin of the original navigation. If a user originally navigated to a page via the omnibox or a bookmark (e.g., chrome://settings), the initiator_origin for that history entry is null. A malicious page can later execute a renderer-initiated history navigation (window.history.back()) to that entry. This new navigation will correctly report IsRendererInitiated() == true, but its initiator_origin will be nullopt. This allows the renderer-initiated navigation to highly privileged chrome:// URLs to completely bypass MayActOnUrl.
Potential Attack Scenario
Note: These are suggested/potential steps to trigger the vulnerability. Our tooling agent does not currently have the ability to run code or verify a full proof-of-concept.
- Setup: An attacker site (
https://attacker.com/) ensures a sensitive, browser-initiated page (likechrome://settings) immediately precedes it in the tab’s session history (e.g., by tricking the user into navigating from a settings page or exploiting a popup flow). - Prompt Injection: The attacker page contains a prompt injection payload instructing the Glic AI to read the current page’s content and exfiltrate it to the attacker’s server.
- Activation: The user activates the Glic AI Actor on the attacker’s tab.
- Navigation: While the AI is preparing or processing its first action, the attacker’s page executes
window.history.back()via JavaScript. - Bypass: The history navigation inherits the null
initiator_originfrom the original browser-initiated navigation.ActorNavigationThrottleincorrectly interprets this as a browser-initiated navigation and allows it to proceed, bypassing theMayActOnUrlscheme block. - Data Capture: The navigation causes the AI’s current action sequence to fail with
kCrossOriginNavigation. However, theExecutionEnginegracefully handles the failure, andGlicActorTaskManager::PerformActionsFinishedcollects a fresh observation of the newly committedchrome://settingspage to update the AI’s context. - Exfiltration: In its next turn, the AI processes the prompt injection against the newly observed
chrome://settingsdata. While it is blocked from executing actions on thechrome://tab, it can open a new tab or use another allowed tab to exfiltrate the sensitive information back to the attacker.
Suggested Fix
The throttle should rely on navigation_handle()->IsRendererInitiated() rather than the presence of an initiator_origin to determine if a navigation was triggered by the page.
if (!is_redirection && !navigation_handle()->IsRendererInitiated()) {
// Proceed for browser-initiated navigations
return content::NavigationThrottle::PROCEED;
}
Evaluated with Chrome root at commit: 661452647ddb2827305122ff3273bd5dea403f09
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.
Raised in root component due to access or custom field issues on 1707859