CVE-2026-106266
Overview
Files Changed
chrome/browser/contextual_tasks/contextual_tasks_ui_service.ccchrome/browser/contextual_tasks/contextual_tasks_ui_service_unittest.cc
Patch
From 046a4f465be8cb0725317ec8424f394c97e55dc8 Mon Sep 17 00:00:00 2001 From: Duncan Mercer <[email protected]> Date: Mon, 14 Sep 2026 18:51:37 -0700 Subject: [PATCH] [Contextual Tasks] Restrict guest link schemes Links originating from the embedded <webview> guest can be re-dispatched via OnThreadLinkClicked or OpenUrl in a new tab without carrying over the guest navigation context. For non-web schemes, this can bypass navigation throttles and web_accessible_resources checks. - Add an early return in ContextualTasksUiService::HandleNavigationImpl to reject non-HTTP(S) and non-about:blank URLs from the embedded guest before dispatching to thread link / OpenUrl helpers. - Add unit tests verifying that chrome-extension:// URLs from the embedded guest are not intercepted, with and without kAimTriggeredThreadLinks. Fixed: b:501914204 Change-Id: If880ef3c43b779b5bc341c4aa5320c9f59a2d609 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8402735 Commit-Queue: Duncan Mercer <[email protected]> Reviewed-by: Juan Mojica <[email protected]> Cr-Commit-Position: refs/heads/main@{#1697728} --- diff --git a/chrome/browser/contextual_tasks/contextual_tasks_ui_service.cc b/chrome/browser/contextual_tasks/contextual_tasks_ui_service.cc index 6a66bcd5..4406827b 100644 --- a/chrome/browser/contextual_tasks/contextual_tasks_ui_service.cc +++ b/chrome/browser/contextual_tasks/contextual_tasks_ui_service.cc @@ -2132,6 +2132,21 @@ } } + // Link navigation helpers below (OnThreadLinkClicked / OpenUrl) issue + // a fresh load in a new tab without carrying over the guest renderer's + // navigation context (initiator origin, renderer-initiated flag). + // Restrict them to HTTP(S) and about:blank (used for window tracking). + // All other schemes (e.g. chrome-extension://, chrome://, file://, + // data:, javascript:) should not be re-dispatched by the browser and are + // instead left to the standard navigation flow and navigation throttles. + if (!url_params.url.SchemeIsHTTPOrHTTPS() && + !url_params.url.IsAboutBlank()) { + OMNIBOX_LOG("nav_trace") + << "ContextualTasks navigation trace: HandleNavigationImpl " + "returning false, non-web scheme from embedded page"; + return false; + } + // On mobile phones without window tracking, link navigations that request // new window creation cannot create separate windows. Intercept them here // and route to `OnThreadLinkClicked` for bottom-sheet handling, whereas diff --git a/chrome/browser/contextual_tasks/contextual_tasks_ui_service_unittest.cc b/chrome/browser/contextual_tasks/contextual_tasks_ui_service_unittest.cc index 55b4eff..834554d 100644 --- a/chrome/browser/contextual_tasks/contextual_tasks_ui_service_unittest.cc +++ b/chrome/browser/contextual_tasks/contextual_tasks_ui_service_unittest.cc @@ -1563,6 +1563,80 @@ run_loop.Run(); } +TEST_F(ContextualTasksUiServiceTest, + Navigation_NonWebScheme_FromEmbeddedGuest_NotIntercepted) { + base::test::ScopedFeatureList scoped_feature_list; + // Test both with kAimTriggeredThreadLinks enabled and disabled. + scoped_feature_list.InitAndEnableFeature(kAimTriggeredThreadLinks); + + GURL extension_url("chrome-extension://someextensionid/secret.html"); + GURL host_web_content_url(chrome::kChromeUIContextualTasksURL); + + ON_CALL(*aim_eligibility_service_, IsAimUrl(_, _)) + .WillByDefault(Return(false)); + ON_CALL(*aim_eligibility_service_, IsAimHost(_, _)) + .WillByDefault(Return(false)); + + auto web_contents = content::WebContentsTester::CreateTestWebContents( + profile_.get(), content::SiteInstance::Create(profile_.get())); + content::WebContentsTester::For(web_contents.get()) + ->SetLastCommittedURL(host_web_content_url); + tabs::MockTabInterface tab; + ON_CALL(tab, GetContents).WillByDefault(Return(web_contents.get())); + + EXPECT_CALL(*service_for_nav_, OnThreadLinkClicked(_, _, _, _, _)).Times(0); + EXPECT_CALL(*service_for_nav_, OpenUrl(_, _, _)).Times(0); + EXPECT_CALL(*service_for_nav_, OnNonThreadNavigationInTab(_, _)).Times(0); + EXPECT_CALL(*service_for_nav_, OnNavigationToAiPageIntercepted(_, _, _)) + .Times(0); + + // When viewed in a tab with a non-web scheme, HandleNavigationImpl should + // return false and avoid dispatching any thread link or OpenUrl helpers. + EXPECT_FALSE(service_for_nav_->HandleNavigationImpl( + CreateOpenUrlParams(extension_url, true), web_contents.get(), &tab, + /*is_from_embedded_page=*/true, + /*from_can_create_window=*/false, /*is_same_site_or_from_ui=*/true, false, + std::nullopt, std::nullopt, blink::mojom::WindowFeatures())); + + // Same check when viewed in side panel (null tab). + EXPECT_FALSE(service_for_nav_->HandleNavigationImpl( + CreateOpenUrlParams(extension_url, true), web_contents.get(), nullptr, + /*is_from_embedded_page=*/true, + /*from_can_create_window=*/false, /*is_same_site_or_from_ui=*/true, false, + std::nullopt, std::nullopt, blink::mojom::WindowFeatures())); +} + +TEST_F(ContextualTasksUiServiceTest, + Navigation_NonWebScheme_AimTriggeredThreadLinksDisabled_NotIntercepted) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndDisableFeature(kAimTriggeredThreadLinks); + + GURL extension_url("chrome-extension://someextensionid/secret.html"); + GURL host_web_content_url(chrome::kChromeUIContextualTasksURL); + + ON_CALL(*aim_eligibility_service_, IsAimUrl(_, _)) + .WillByDefault(Return(false)); + ON_CALL(*aim_eligibility_service_, IsAimHost(_, _)) + .WillByDefault(Return(false)); + + auto web_contents = content::WebContentsTester::CreateTestWebContents( + profile_.get(), content::SiteInstance::Create(profile_.get())); + content::WebContentsTester::For(web_contents.get()) + ->SetLastCommittedURL(host_web_content_url); + + EXPECT_CALL(*service_for_nav_, OnThreadLinkClicked(_, _, _, _, _)).Times(0); + EXPECT_CALL(*service_for_nav_, OpenUrl(_, _, _)).Times(0); + EXPECT_CALL(*service_for_nav_, OnNonThreadNavigationInTab(_, _)).Times(0); + EXPECT_CALL(*service_for_nav_, OnNavigationToAiPageIntercepted(_, _, _)) + .Times(0); + + EXPECT_FALSE(service_for_nav_->HandleNavigationImpl( + CreateOpenUrlParams(extension_url, true), web_contents.get(), nullptr, + /*is_from_embedded_page=*/true, + /*from_can_create_window=*/false, /*is_same_site_or_from_ui=*/true, false, + std::nullopt, std::nullopt, blink::mojom::WindowFeatures())); +} + // If the search results page is navigated to while viewing the UI in the side // panel (e.g. no tab tied to the WebContents), ensure the correct event is // fired.
Regression Test / PoC
diff --git a/chrome/browser/contextual_tasks/contextual_tasks_ui_service_unittest.cc b/chrome/browser/contextual_tasks/contextual_tasks_ui_service_unittest.cc
index 55b4eff..834554d 100644
--- a/chrome/browser/contextual_tasks/contextual_tasks_ui_service_unittest.cc
+++ b/chrome/browser/contextual_tasks/contextual_tasks_ui_service_unittest.cc
@@ -1563,6 +1563,80 @@
run_loop.Run();
}
+TEST_F(ContextualTasksUiServiceTest,
+ Navigation_NonWebScheme_FromEmbeddedGuest_NotIntercepted) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ // Test both with kAimTriggeredThreadLinks enabled and disabled.
+ scoped_feature_list.InitAndEnableFeature(kAimTriggeredThreadLinks);
+
+ GURL extension_url("chrome-extension://someextensionid/secret.html");
+ GURL host_web_content_url(chrome::kChromeUIContextualTasksURL);
+
+ ON_CALL(*aim_eligibility_service_, IsAimUrl(_, _))
+ .WillByDefault(Return(false));
+ ON_CALL(*aim_eligibility_service_, IsAimHost(_, _))
+ .WillByDefault(Return(false));
+
+ auto web_contents = content::WebContentsTester::CreateTestWebContents(
+ profile_.get(), content::SiteInstance::Create(profile_.get()));
+ content::WebContentsTester::For(web_contents.get())
+ ->SetLastCommittedURL(host_web_content_url);
+ tabs::MockTabInterface tab;
+ ON_CALL(tab, GetContents).WillByDefault(Return(web_contents.get()));
+
+ EXPECT_CALL(*service_for_nav_, OnThreadLinkClicked(_, _, _, _, _)).Times(0);
+ EXPECT_CALL(*service_for_nav_, OpenUrl(_, _, _)).Times(0);
+ EXPECT_CALL(*service_for_nav_, OnNonThreadNavigationInTab(_, _)).Times(0);
+ EXPECT_CALL(*service_for_nav_, OnNavigationToAiPageIntercepted(_, _, _))
+ .Times(0);
+
+ // When viewed in a tab with a non-web scheme, HandleNavigationImpl should
+ // return false and avoid dispatching any thread link or OpenUrl helpers.
+ EXPECT_FALSE(service_for_nav_->HandleNavigationImpl(
+ CreateOpenUrlParams(extension_url, true), web_contents.get(), &tab,
+ /*is_from_embedded_page=*/true,
+ /*from_can_create_window=*/false, /*is_same_site_or_from_ui=*/true, false,
+ std::nullopt, std::nullopt, blink::mojom::WindowFeatures()));
+
+ // Same check when viewed in side panel (null tab).
+ EXPECT_FALSE(service_for_nav_->HandleNavigationImpl(
+ CreateOpenUrlParams(extension_url, true), web_contents.get(), nullptr,
+ /*is_from_embedded_page=*/true,
+ /*from_can_create_window=*/false, /*is_same_site_or_from_ui=*/true, false,
+ std::nullopt, std::nullopt, blink::mojom::WindowFeatures()));
+}
+
+TEST_F(ContextualTasksUiServiceTest,
+ Navigation_NonWebScheme_AimTriggeredThreadLinksDisabled_NotIntercepted) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndDisableFeature(kAimTriggeredThreadLinks);
+
+ GURL extension_url("chrome-extension://someextensionid/secret.html");
+ GURL host_web_content_url(chrome::kChromeUIContextualTasksURL);
+
+ ON_CALL(*aim_eligibility_service_, IsAimUrl(_, _))
+ .WillByDefault(Return(false));
+ ON_CALL(*aim_eligibility_service_, IsAimHost(_, _))
+ .WillByDefault(Return(false));
+
+ auto web_contents = content::WebContentsTester::CreateTestWebContents(
+ profile_.get(), content::SiteInstance::Create(profile_.get()));
+ content::WebContentsTester::For(web_contents.get())
+ ->SetLastCommittedURL(host_web_content_url);
+
+ EXPECT_CALL(*service_for_nav_, OnThreadLinkClicked(_, _, _, _, _)).Times(0);
+ EXPECT_CALL(*service_for_nav_, OpenUrl(_, _, _)).Times(0);
+ EXPECT_CALL(*service_for_nav_, OnNonThreadNavigationInTab(_, _)).Times(0);
+ EXPECT_CALL(*service_for_nav_, OnNavigationToAiPageIntercepted(_, _, _))
+ .Times(0);
+
+ EXPECT_FALSE(service_for_nav_->HandleNavigationImpl(
+ CreateOpenUrlParams(extension_url, true), web_contents.get(), nullptr,
+ /*is_from_embedded_page=*/true,
+ /*from_can_create_window=*/false, /*is_same_site_or_from_ui=*/true, false,
+ std::nullopt, std::nullopt, blink::mojom::WindowFeatures()));
+}
+
// If the search results page is navigated to while viewing the UI in the side
// panel (e.g. no tab tied to the WebContents), ensure the correct event is
// fired.
Original Bug Report
Potential extension web_accessible_resources bypass via Contextual Tasks
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.
Overview: A compromised guest renderer within the Contextual Tasks WebUI can bypass extension security policies by initiating a new window request. The Contextual Tasks service intercepts the request and re-issues it as a browser-initiated navigation without an initiator origin. This launders the navigation, allowing it to bypass web_accessible_resources checks and load restricted extension pages.
Affected files:
chrome/browser/contextual_tasks/contextual_tasks_ui_service.ccchrome/browser/chrome_content_browser_client.ccextensions/browser/extension_navigation_throttle.cccontent/browser/renderer_host/render_frame_host_impl.cc
Estimated timestamp from git blame: 2026-04-08
Summary
A potential vulnerability exists in the Contextual Tasks feature where a compromised guest renderer can bypass web_accessible_resources restrictions for installed Chrome extensions. When the guest renderer requests a new window, the ContextualTasksUiService intercepts the request and manually triggers the navigation using a newly instantiated content::NavigationController::LoadURLParams. Because it does not propagate the original initiator_origin or set is_renderer_initiated, the browser treats the request as a trusted, browser-initiated navigation, bypassing extension security boundaries.
Note: The following steps and analysis are suggested/potential scenarios based on source code analysis, as our tooling agent does not currently run active exploit code to verify them dynamically.
Potential Attack Scenario
- An attacker compromises the sandboxed guest renderer used by the Contextual Tasks feature (e.g., via an XSS payload in the web content it loads).
- The attacker executes JavaScript to open a new window pointing to a sensitive, non-web-accessible internal page of an installed extension:
window.open("chrome-extension://<target_id>/secret_page.html"). - The browser process receives the
mojom::Frame::CreateNewWindowIPC. - The
chrome-extension://URL passes the initialFilterURLcheck because the scheme is registered as aWebSafeIsolatedscheme, making it generally requestable. - The Contextual Tasks service intercepts the request, aborts the standard window creation, and opens the URL in a new tab as a browser-initiated navigation.
- The new tab successfully loads the internal extension page, bypassing the extension’s manifest restrictions and exposing its privileged context to the attacker.
Root Cause Analysis
The vulnerability stems from a loss of navigation context during request interception.
- Interception: In
chrome/browser/chrome_content_browser_client.cc:4425(ChromeContentBrowserClient::CanCreateWindow), the Contextual Tasks feature intercepts thewindow.opencall. It passesis_from_embedded_page = trueandis_to_new_tab = truetocontextual_tasks_ui_service->HandleNavigation(). - Forced Link Handling: Inside
ContextualTasksUiService::HandleNavigationImpl, the combination ofis_from_embedded_pageandis_to_new_tabforces theis_allowed_hostcheck to evaluate tofalse(line 925). Consequently, the code falls through to line 979, posting a task toOnThreadLinkClickedand returningtrueto abort standard window creation. - Context Loss (Laundering): In
OnThreadLinkClicked(chrome/browser/contextual_tasks/contextual_tasks_ui_service.cc:474), a new tab is created and navigated using:new_contents->GetController().LoadURLWithParams(content::NavigationController::LoadURLParams(url));By default, theLoadURLParamsconstructor initializesis_renderer_initiatedtofalseandinitiator_origintostd::nullopt. - Policy Bypass: As the navigation starts,
ExtensionNavigationThrottle::WillStartOrRedirectRequest(extensions/browser/extension_navigation_throttle.cc:356) checks if there is aninitiator_origin. Seeingnullopt, it treats the navigation as an “Automatically trusted” browser-initiated navigation (like a user typing in the omnibox) and returnscontent::NavigationThrottle::PROCEED. This fully bypasses theweb_accessible_resourceschecks.
Suggested Fix
When constructing LoadURLParams in ContextualTasksUiService::OnThreadLinkClicked (or the methods calling it), the original navigation context must be preserved.
The HandleNavigation method should accept the initiator_origin from the source_contents, and this origin should be explicitly assigned to LoadURLParams::initiator_origin. Additionally, LoadURLParams::is_renderer_initiated should be set to true when the original action was renderer-initiated. Alternatively, HandleNavigationImpl should explicitly reject unhandled or sensitive schemes (like chrome-extension://) before treating them as thread link clicks.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
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 1516765