Chrome · Google Lens
CVE-2026-106228
Authorization Error in Google Lens
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/lens/lens_overlay_side_panel_web_view.cc |
modified | |
switchchrome/browser/ui/lens/lens_overlay_side_panel_web_view.cc |
modified | |
forchrome/browser/ui/lens/lens_overlay_controller_browsertest.cc |
modified |
Files Changed
chrome/browser/ui/lens/lens_overlay_side_panel_web_view.ccchrome/browser/ui/lens/lens_overlay_controller_browsertest.cc
Patch
From 259ed9d14bd89ed5eb1357fb87d907c4e732e5f0 Mon Sep 17 00:00:00 2001 From: Juan Mojica <[email protected]> Date: Tue, 15 Sep 2026 17:21:45 -0700 Subject: [PATCH] Validate navigation parameters in Lens overlay side panel LensOverlaySidePanelWebView::OpenURLFromTab forwarded incoming OpenURLParams directly to BrowserWindowInterface::OpenURL. That path supplies a null source WebContents, so the popup blocker never ran for navigations requested from the side panel, and the unvalidated parameters could cause mismatched FrameTreeNode lookups when a subframe requested a navigation targeting another frame tree. Validate requests that originate from the results frame, which renders remotely served content: - Restrict forwarded navigations to HTTP and HTTPS schemes. - Confirm a claimed user gesture against transient activation in the initiating RenderFrameHost, ignoring any initiator that does not belong to this side panel. - Demote dispositions that the popup blocker does not evaluate, notably SAVE_TO_DISK and OFF_THE_RECORD, to NEW_FOREGROUND_TAB so that the underlying tab is never navigated and downloads cannot be started without a gesture. - Reset the frame tree node identifier. Validated requests are then routed through the tab's WebContents delegate instead of the browser window interface, which attributes the navigation to the tab so that it is evaluated against popup blocking rules. Consequently the popup content setting is now evaluated against the tab's URL. Requests from the side panel WebUI main frame originate from browser-controlled code and continue to be forwarded unmodified. Bug: 517689673 Change-Id: I89f62b3e84dc7c0d81d6a73fb55c0860c1cb229e Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8414217 Commit-Queue: Juan Mojica <[email protected]> Reviewed-by: Bryan Nguyen <[email protected]> Cr-Commit-Position: refs/heads/main@{#1698537} --- diff --git a/chrome/browser/ui/lens/lens_overlay_side_panel_web_view.cc b/chrome/browser/ui/lens/lens_overlay_side_panel_web_view.cc index 9f3324d..2f8ff2e 100644 --- a/chrome/browser/ui/lens/lens_overlay_side_panel_web_view.cc +++ b/chrome/browser/ui/lens/lens_overlay_side_panel_web_view.cc @@ -13,9 +13,15 @@ #include "chrome/common/webui_url_constants.h" #include "chrome/grit/generated_resources.h" #include "components/input/native_web_keyboard_event.h" +#include "components/tabs/public/tab_interface.h" #include "components/web_modal/web_contents_modal_dialog_manager.h" #include "content/public/browser/file_select_listener.h" +#include "content/public/browser/global_routing_id.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_contents_delegate.h" #include "ui/base/metadata/metadata_impl_macros.h" +#include "ui/base/window_open_disposition.h" using SidePanelWebUIViewT_LensSidePanelUntrustedUI = SidePanelWebUIViewT<lens::LensSidePanelUntrustedUI>; @@ -73,10 +79,115 @@ const content::OpenURLParams& params, base::OnceCallback<void(content::NavigationHandle&)> navigation_handle_callback) { - coordinator_->GetLensSearchController() - ->GetTabInterface() - ->GetBrowserWindowInterface() - ->OpenURL(params, std::move(navigation_handle_callback)); + // Note that `navigation_handle_callback` is dropped without being run on all + // of the early returns below. Callers must tolerate the callback never + // running, which is the same contract as any other rejected navigation. + if (!coordinator_ || !coordinator_->GetLensSearchController()) { + return nullptr; + } + tabs::TabInterface* tab = + coordinator_->GetLensSearchController()->GetTabInterface(); + // A tab that is not attached to a browser window has nowhere to open the + // URL, so drop the request instead of navigating. + if (!tab || !tab->GetBrowserWindowInterface()) { + return nullptr; + } + + // Resolve the frame that requested this navigation. + // `initiator_frame_token` is preferred because it identifies the frame even + // if it has since navigated; the routing ID is only a fallback for requests + // that do not carry a token. + content::RenderFrameHost* source_rfh = nullptr; + if (params.initiator_frame_token.has_value()) { + source_rfh = content::RenderFrameHost::FromFrameToken( + content::GlobalRenderFrameHostToken( + params.initiator_process_id, params.initiator_frame_token.value())); + } else { + source_rfh = content::RenderFrameHost::FromID( + params.source_render_process_id, params.source_render_frame_id); + } + // Only an initiator that actually lives in this side panel may be trusted; + // treat anything else as an unknown initiator so that its state (notably + // user activation) is not attributed to this side panel. + if (source_rfh && content::WebContents::FromRenderFrameHost(source_rfh) != + GetWebContents()) { + source_rfh = nullptr; + } + + // Everything hosted by this WebContents is renderer content that the browser + // must not trust, including the chrome-untrusted:// WebUI main frame itself, + // so the scheme check below applies to every initiator without exception. + // + // The side panel only renders links from web content, so restrict forwarded + // navigations to web schemes. This intentionally also drops external + // protocol links (e.g. mailto:). + if (!params.url.SchemeIsHTTPOrHTTPS()) { + return nullptr; + } + + content::OpenURLParams modified_params = params; + + // The two checks below exist to catch a renderer lying about how a navigation + // was requested. They do not apply to requests the browser itself created, + // such as a context menu command, because those values are not renderer + // claims in the first place. + // + // A renderer hosted here cannot forge this bit. Navigator::RequestOpenURL() + // hardcodes `is_renderer_initiated` to true, and only clears it for a WebUI + // whose TrustPolicy is kTrusted. The side panel is LensSidePanelUntrustedUI, + // an UntrustedWebUIController, which is kUntrusted, and the results frame is + // ordinary web content with no WebUI at all. + if (params.is_renderer_initiated) { + // Check that user_gesture is really true, by confirming that there was a + // recent activation in the source render frame host. This is best effort: + // an unknown initiator is treated as having no activation. + if (modified_params.user_gesture && + (!source_rfh || !source_rfh->HasTransientUserActivation())) { + modified_params.user_gesture = false; + } + + // Only forward dispositions that open a new container and that + // `blocked_content::ConsiderForPopupBlocking()` actually evaluates. + // Anything else is demoted to NEW_FOREGROUND_TAB so that the popup blocker + // still runs and the underlying tab is never navigated. Notably, + // SAVE_TO_DISK and OFF_THE_RECORD are deliberately excluded: the popup + // blocker ignores both, so allowing them would let the results frame start + // downloads or open off-the-record windows with no user gesture. + // + // `default` is used deliberately so that any disposition added in the + // future is demoted rather than silently forwarded. + switch (modified_params.disposition) { + case WindowOpenDisposition::NEW_FOREGROUND_TAB: + case WindowOpenDisposition::NEW_BACKGROUND_TAB: + case WindowOpenDisposition::NEW_POPUP: + case WindowOpenDisposition::NEW_WINDOW: + break; + default: + modified_params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB; + break; + } + } + + // Reset frame_tree_node_id to prevent mismatched frame tree lookups in the + // target navigation controller. + modified_params.frame_tree_node_id = content::FrameTreeNodeId(); + + // Pass the main tab's WebContents as the source so the navigation pipeline + // has the correct context to evaluate disposition and blocking rules. + content::WebContents* tab_contents = tab->GetContents(); + if (tab_contents && tab_contents->GetDelegate()) { + tab_contents->GetDelegate()->OpenURLFromTab( + tab_contents, modified_params, std::move(navigation_handle_callback)); + } + + // Always return nullptr, even when the navigation succeeded and created a + // WebContents. Returning it would make WebContentsImpl::OpenURL() notify + // WebContentsObserver::DidOpenRequestedURL(), and + // LensOverlaySidePanelCoordinator observes this WebContents and reacts to + // that notification by opening the URL in the browser itself. The URL would + // then be opened twice for a single request. That observer exists to service + // the WebContentsImpl::CreateNewWindow() path (e.g. target="_blank"), which + // this method does not go through. return nullptr; } diff --git a/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc b/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc index 0e8d3981..4e196e86 100644 --- a/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc +++ b/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc @@ -17,6 +17,7 @@ #include "base/files/file_util.h" #include "base/memory/raw_ptr_exclusion.h" #include "base/path_service.h" +#include "base/run_loop.h" #include "base/strings/strcat.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" @@ -103,6 +104,7 @@ #include "chrome/test/base/in_process_browser_test.h" #include "chrome/test/base/ui_test_utils.h" #include "components/base32/base32.h" +#include "components/blocked_content/popup_blocker_tab_helper.h" #include "components/constrained_window/constrained_window_views.h" #include "components/contextual_tasks/public/features.h" #include "components/feature_engagement/public/feature_constants.h" @@ -912,6 +914,93 @@ lens::WaitForPaint(browser(), url, disposition, browser_test_flags); } + // Opens the overlay and the results side panel, then returns the results + // frame hosted inside the side panel WebContents. Returns nullptr if any step + // fails; callers should ASSERT on the result before using it. + content::RenderFrameHost* OpenSidePanelAndGetResultsFrame() { + auto* controller = GetLensOverlayController(); + OpenLensOverlay(LensOverlayInvocationSource::kAppMenu); + if (!base::test::RunUntil( + [&]() { return controller->state() == State::kOverlay; })) { + return nullptr; + } + if (!content::WaitForLoadStop(GetOverlayWebContents())) { + return nullptr; + } + + controller->OpenSidePanelForTesting(); + GetLensOverlaySidePanelCoordinator()->LoadURLInResultsFrameForTesting( + GURL("https://www.google.com/search")); + if (!IsLensResultsSidePanelShowing()) { + return nullptr; + } + content::WebContents* side_panel_contents = + controller->GetSidePanelWebContentsForTesting(); + if (!side_panel_contents || + !content::WaitForLoadStop(side_panel_contents)) { + return nullptr; + } + + // The results frame is the only child frame of the side panel web contents. + return content::ChildFrameAt(side_panel_contents->GetPrimaryMainFrame(), 0); + } + + // Builds the OpenURLParams that the browser would construct for a navigation + // requested by `initiator`. + content::OpenURLParams MakeRendererInitiatedParams( + content::RenderFrameHost* initiator, + const GURL& url, + WindowOpenDisposition disposition, + bool user_gesture, + content::FrameTreeNodeId frame_tree_node_id = + content::FrameTreeNodeId()) { + content::OpenURLParams params(url, content::Referrer(), frame_tree_node_id, + disposition, ui::PAGE_TRANSITION_LINK, + /*is_renderer_initiated=*/true); + params.user_gesture = user_gesture; + params.initiator_origin = initiator->GetLastCommittedOrigin(); + params.initiator_frame_token = initiator->GetFrameToken(); + params.initiator_process_id = initiator->GetProcess()->GetDeprecatedID(); + params.source_site_instance = initiator->GetSiteInstance(); + params.source_render_process_id = + initiator->GetProcess()->GetDeprecatedID(); + params.source_render_frame_id = initiator->GetRoutingID(); + return params; + } + + // Builds the OpenURLParams that RenderViewContextMenuBase constructs when a + // link inside `initiator` is right clicked and a command such as "Open link + // in new tab" is chosen. These are browser initiated, which is why + // `user_gesture` defaults to true. + content::OpenURLParams MakeContextMenuParams( + content::RenderFrameHost* initiator, + const GURL& url, + WindowOpenDisposition disposition) { + content::OpenURLParams params = + content::OpenURLParams::CreateBrowserInitiated( + url, disposition, ui::PAGE_TRANSITION_LINK, content::Referrer(), + /*started_from_context_menu=*/true); + params.source_render_process_id = + initiator->GetProcess()->GetDeprecatedID(); + params.source_render_frame_id = initiator->GetRoutingID(); + params.initiator_frame_token = initiator->GetFrameToken(); + params.initiator_process_id = initiator->GetProcess()->GetDeprecatedID(); + params.initiator_origin = initiator->GetLastCommittedOrigin(); + params.source_site_instance = initiator->GetSiteInstance(); + return params; + } + + // Runs every task that was already posted to this thread's task queue. Task + // runners are FIFO, so once the closure posted below runs, anything posted + // before it has run too. This is used to assert that a task which would have + // opened an extra tab did not do so. + void WaitForPreviouslyPostedTasks() { + base::RunLoop run_loop; + base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, run_loop.QuitClosure()); + run_loop.Run(); + } + // Helper to remove the start time, client upload duration, and viewport size // query params from the url. GURL RemoveStartTimeAndSizeParams(const GURL& url_to_process) { @@ -2634,6 +2723,408 @@ .ExtractBool()); } +IN_PROC_BROWSER_TEST_F( + LensOverlayControllerBrowserTest, + SidePanel_OpenURLFromTab_CurrentTabFromSubframe_DoesNotNavigateCurrentTab) { + WaitForPaint(); + + content::RenderFrameHost* results_frame = OpenSidePanelAndGetResultsFrame(); + ASSERT_TRUE(results_frame); + ASSERT_TRUE(results_frame->GetParentOrOuterDocument()); + content::WebContents* side_panel_contents = + GetLensOverlayController()->GetSidePanelWebContentsForTesting(); + ASSERT_TRUE(side_panel_contents); + + content::WebContents* active_tab = + browser()->GetTabStripModel()->GetActiveWebContents(); + const GURL initial_active_url = active_tab->GetLastCommittedURL(); + const int initial_tab_count = browser()->GetTabStripModel()->count(); + + // Request a CURRENT_TAB navigation carrying the results frame's frame tree + // node id, which belongs to the side panel rather than to the tab being + // navigated. + const content::OpenURLParams params = MakeRendererInitiatedParams( + results_frame, embedded_test_server()->GetURL("/title1.html"), + WindowOpenDisposition::CURRENT_TAB, /*user_gesture=*/true, + results_frame->GetFrameTreeNodeId()); + + // The request should be handled safely without crashing on frame tree + // lookups or navigating the current active tab. + side_panel_contents->OpenURL(params, /*navigation_handle_callback=*/{}); + + // Verify the active tab was not navigated, not even asynchronously. + EXPECT_FALSE(active_tab->GetController().GetPendingEntry()); + EXPECT_EQ(initial_active_url, active_tab->GetLastCommittedURL()); + EXPECT_EQ(initial_tab_count, browser()->GetTabStripModel()->count()); +} + +IN_PROC_BROWSER_TEST_F( + LensOverlayControllerBrowserTest, + SidePanel_OpenURLFromTab_WithoutUserActivation_BlocksPopup) { + WaitForPaint(); + + content::RenderFrameHost* results_frame = OpenSidePanelAndGetResultsFrame(); + ASSERT_TRUE(results_frame); + content::WebContents* side_panel_contents = + GetLensOverlayController()->GetSidePanelWebContentsForTesting(); + ASSERT_TRUE(side_panel_contents); + + const int initial_tab_count = browser()->GetTabStripModel()->count(); + auto* popup_blocker = blocked_content::PopupBlockerTabHelper::FromWebContents( + browser()->GetTabStripModel()->GetActiveWebContents()); + ASSERT_TRUE(popup_blocker); + + // Send OpenURL requests with user_gesture = false and no transient + // activation anywhere. + const WindowOpenDisposition kDispositions[] = { + WindowOpenDisposition::NEW_BACKGROUND_TAB, + WindowOpenDisposition::NEW_FOREGROUND_TAB, + }; + + size_t expected_blocked_popups = 0; + for (WindowOpenDisposition disposition : kDispositions) { + SCOPED_TRACE(testing::Message() + << "disposition " << static_cast<int>(disposition)); + const content::OpenURLParams params = MakeRendererInitiatedParams( + results_frame, embedded_test_server()->GetURL("/title1.html"), + disposition, /*user_gesture=*/false); + + side_panel_contents->OpenURL(params, /*navigation_handle_callback=*/{}); + + // Verify that the request is evaluated against popup blocking rules and + // does not open a new tab. + ++expected_blocked_popups; + EXPECT_EQ(initial_tab_count, browser()->GetTabStripModel()->count()); + EXPECT_EQ(expected_blocked_popups, popup_blocker->GetBlockedPopupsCount()); + } +} + +// The side panel WebUI main frame is chrome-untrusted:// content, so requests +// it initiates must be validated exactly like requests from the results frame. +IN_PROC_BROWSER_TEST_F( + LensOverlayControllerBrowserTest, + SidePanel_OpenURLFromTab_CurrentTabFromWebUIMainFrame_IsAlsoValidated) { + WaitForPaint(); + + ASSERT_TRUE(OpenSidePanelAndGetResultsFrame()); + content::WebContents* side_panel_contents = + GetLensOverlayController()->GetSidePanelWebContentsForTesting(); + ASSERT_TRUE(side_panel_contents); + content::RenderFrameHost* main_frame = + side_panel_contents->GetPrimaryMainFrame(); + ASSERT_TRUE(main_frame); + + content::WebContents* active_tab = + browser()->GetTabStripModel()->GetActiveWebContents(); + const GURL initial_active_url = active_tab->GetLastCommittedURL(); + const int initial_tab_count = browser()->GetTabStripModel()->count(); + auto* popup_blocker = + blocked_content::PopupBlockerTabHelper::FromWebContents(active_tab); + ASSERT_TRUE(popup_blocker); + + const content::OpenURLParams params = MakeRendererInitiatedParams( + main_frame, embedded_test_server()->GetURL("/title1.html"), + WindowOpenDisposition::CURRENT_TAB, /*user_gesture=*/true, + main_frame->GetFrameTreeNodeId()); ... (file diff truncated)
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc b/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc
index 0e8d3981..4e196e86 100644
--- a/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc
+++ b/chrome/browser/ui/lens/lens_overlay_controller_browsertest.cc
@@ -17,6 +17,7 @@
#include "base/files/file_util.h"
#include "base/memory/raw_ptr_exclusion.h"
#include "base/path_service.h"
+#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
@@ -103,6 +104,7 @@
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/base32/base32.h"
+#include "components/blocked_content/popup_blocker_tab_helper.h"
#include "components/constrained_window/constrained_window_views.h"
#include "components/contextual_tasks/public/features.h"
#include "components/feature_engagement/public/feature_constants.h"
@@ -912,6 +914,93 @@
lens::WaitForPaint(browser(), url, disposition, browser_test_flags);
}
+ // Opens the overlay and the results side panel, then returns the results
+ // frame hosted inside the side panel WebContents. Returns nullptr if any step
+ // fails; callers should ASSERT on the result before using it.
+ content::RenderFrameHost* OpenSidePanelAndGetResultsFrame() {
+ auto* controller = GetLensOverlayController();
+ OpenLensOverlay(LensOverlayInvocationSource::kAppMenu);
+ if (!base::test::RunUntil(
+ [&]() { return controller->state() == State::kOverlay; })) {
+ return nullptr;
+ }
+ if (!content::WaitForLoadStop(GetOverlayWebContents())) {
+ return nullptr;
+ }
+
+ controller->OpenSidePanelForTesting();
+ GetLensOverlaySidePanelCoordinator()->LoadURLInResultsFrameForTesting(
+ GURL("https://www.google.com/search"));
+ if (!IsLensResultsSidePanelShowing()) {
+ return nullptr;
+ }
+ content::WebContents* side_panel_contents =
+ controller->GetSidePanelWebContentsForTesting();
+ if (!side_panel_contents ||
+ !content::WaitForLoadStop(side_panel_contents)) {
+ return nullptr;
+ }
+
+ // The results frame is the only child frame of the side panel web contents.
+ return content::ChildFrameAt(side_panel_contents->GetPrimaryMainFrame(), 0);
+ }
+
+ // Builds the OpenURLParams that the browser would construct for a navigation
+ // requested by `initiator`.
+ content::OpenURLParams MakeRendererInitiatedParams(
+ content::RenderFrameHost* initiator,
+ const GURL& url,
+ WindowOpenDisposition disposition,
+ bool user_gesture,
+ content::FrameTreeNodeId frame_tree_node_id =
+ content::FrameTreeNodeId()) {
+ content::OpenURLParams params(url, content::Referrer(), frame_tree_node_id,
+ disposition, ui::PAGE_TRANSITION_LINK,
+ /*is_renderer_initiated=*/true);
+ params.user_gesture = user_gesture;
+ params.initiator_origin = initiator->GetLastCommittedOrigin();
+ params.initiator_frame_token = initiator->GetFrameToken();
+ params.initiator_process_id = initiator->GetProcess()->GetDeprecatedID();
+ params.source_site_instance = initiator->GetSiteInstance();
+ params.source_render_process_id =
+ initiator->GetProcess()->GetDeprecatedID();
+ params.source_render_frame_id = initiator->GetRoutingID();
+ return params;
+ }
+
+ // Builds the OpenURLParams that RenderViewContextMenuBase constructs when a
+ // link inside `initiator` is right clicked and a command such as "Open link
+ // in new tab" is chosen. These are browser initiated, which is why
+ // `user_gesture` defaults to true.
+ content::OpenURLParams MakeContextMenuParams(
+ content::RenderFrameHost* initiator,
+ const GURL& url,
+ WindowOpenDisposition disposition) {
+ content::OpenURLParams params =
+ content::OpenURLParams::CreateBrowserInitiated(
+ url, disposition, ui::PAGE_TRANSITION_LINK, content::Referrer(),
+ /*started_from_context_menu=*/true);
+ params.source_render_process_id =
+ initiator->GetProcess()->GetDeprecatedID();
+ params.source_render_frame_id = initiator->GetRoutingID();
+ params.initiator_frame_token = initiator->GetFrameToken();
+ params.initiator_process_id = initiator->GetProcess()->GetDeprecatedID();
+ params.initiator_origin = initiator->GetLastCommittedOrigin();
+ params.source_site_instance = initiator->GetSiteInstance();
+ return params;
+ }
+
+ // Runs every task that was already posted to this thread's task queue. Task
+ // runners are FIFO, so once the closure posted below runs, anything posted
+ // before it has run too. This is used to assert that a task which would have
+ // opened an extra tab did not do so.
+ void WaitForPreviouslyPostedTasks() {
+ base::RunLoop run_loop;
+ base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, run_loop.QuitClosure());
+ run_loop.Run();
+ }
+
// Helper to remove the start time, client upload duration, and viewport size
// query params from the url.
GURL RemoveStartTimeAndSizeParams(const GURL& url_to_process) {
@@ -2634,6 +2723,408 @@
.ExtractBool());
}
+IN_PROC_BROWSER_TEST_F(
+ LensOverlayControllerBrowserTest,
+ SidePanel_OpenURLFromTab_CurrentTabFromSubframe_DoesNotNavigateCurrentTab) {
+ WaitForPaint();
+
+ content::RenderFrameHost* results_frame = OpenSidePanelAndGetResultsFrame();
+ ASSERT_TRUE(results_frame);
+ ASSERT_TRUE(results_frame->GetParentOrOuterDocument());
+ content::WebContents* side_panel_contents =
+ GetLensOverlayController()->GetSidePanelWebContentsForTesting();
+ ASSERT_TRUE(side_panel_contents);
+
+ content::WebContents* active_tab =
+ browser()->GetTabStripModel()->GetActiveWebContents();
+ const GURL initial_active_url = active_tab->GetLastCommittedURL();
+ const int initial_tab_count = browser()->GetTabStripModel()->count();
+
+ // Request a CURRENT_TAB navigation carrying the results frame's frame tree
+ // node id, which belongs to the side panel rather than to the tab being
+ // navigated.
+ const content::OpenURLParams params = MakeRendererInitiatedParams(
+ results_frame, embedded_test_server()->GetURL("/title1.html"),
+ WindowOpenDisposition::CURRENT_TAB, /*user_gesture=*/true,
+ results_frame->GetFrameTreeNodeId());
+
+ // The request should be handled safely without crashing on frame tree
+ // lookups or navigating the current active tab.
+ side_panel_contents->OpenURL(params, /*navigation_handle_callback=*/{});
+
+ // Verify the active tab was not navigated, not even asynchronously.
+ EXPECT_FALSE(active_tab->GetController().GetPendingEntry());
+ EXPECT_EQ(initial_active_url, active_tab->GetLastCommittedURL());
+ EXPECT_EQ(initial_tab_count, browser()->GetTabStripModel()->count());
+}
+
+IN_PROC_BROWSER_TEST_F(
+ LensOverlayControllerBrowserTest,
+ SidePanel_OpenURLFromTab_WithoutUserActivation_BlocksPopup) {
+ WaitForPaint();
+
+ content::RenderFrameHost* results_frame = OpenSidePanelAndGetResultsFrame();
+ ASSERT_TRUE(results_frame);
+ content::WebContents* side_panel_contents =
+ GetLensOverlayController()->GetSidePanelWebContentsForTesting();
+ ASSERT_TRUE(side_panel_contents);
+
+ const int initial_tab_count = browser()->GetTabStripModel()->count();
+ auto* popup_blocker = blocked_content::PopupBlockerTabHelper::FromWebContents(
+ browser()->GetTabStripModel()->GetActiveWebContents());
+ ASSERT_TRUE(popup_blocker);
+
+ // Send OpenURL requests with user_gesture = false and no transient
+ // activation anywhere.
+ const WindowOpenDisposition kDispositions[] = {
+ WindowOpenDisposition::NEW_BACKGROUND_TAB,
+ WindowOpenDisposition::NEW_FOREGROUND_TAB,
+ };
+
+ size_t expected_blocked_popups = 0;
+ for (WindowOpenDisposition disposition : kDispositions) {
+ SCOPED_TRACE(testing::Message()
+ << "disposition " << static_cast<int>(disposition));
+ const content::OpenURLParams params = MakeRendererInitiatedParams(
+ results_frame, embedded_test_server()->GetURL("/title1.html"),
+ disposition, /*user_gesture=*/false);
+
+ side_panel_contents->OpenURL(params, /*navigation_handle_callback=*/{});
+
+ // Verify that the request is evaluated against popup blocking rules and
+ // does not open a new tab.
+ ++expected_blocked_popups;
+ EXPECT_EQ(initial_tab_count, browser()->GetTabStripModel()->count());
+ EXPECT_EQ(expected_blocked_popups, popup_blocker->GetBlockedPopupsCount());
+ }
+}
+
+// The side panel WebUI main frame is chrome-untrusted:// content, so requests
+// it initiates must be validated exactly like requests from the results frame.
+IN_PROC_BROWSER_TEST_F(
+ LensOverlayControllerBrowserTest,
+ SidePanel_OpenURLFromTab_CurrentTabFromWebUIMainFrame_IsAlsoValidated) {
+ WaitForPaint();
+
+ ASSERT_TRUE(OpenSidePanelAndGetResultsFrame());
+ content::WebContents* side_panel_contents =
+ GetLensOverlayController()->GetSidePanelWebContentsForTesting();
+ ASSERT_TRUE(side_panel_contents);
+ content::RenderFrameHost* main_frame =
+ side_panel_contents->GetPrimaryMainFrame();
+ ASSERT_TRUE(main_frame);
+
+ content::WebContents* active_tab =
+ browser()->GetTabStripModel()->GetActiveWebContents();
+ const GURL initial_active_url = active_tab->GetLastCommittedURL();
+ const int initial_tab_count = browser()->GetTabStripModel()->count();
+ auto* popup_blocker =
+ blocked_content::PopupBlockerTabHelper::FromWebContents(active_tab);
+ ASSERT_TRUE(popup_blocker);
+
+ const content::OpenURLParams params = MakeRendererInitiatedParams(
+ main_frame, embedded_test_server()->GetURL("/title1.html"),
+ WindowOpenDisposition::CURRENT_TAB, /*user_gesture=*/true,
+ main_frame->GetFrameTreeNodeId());
+
+ side_panel_contents->OpenURL(params, /*navigation_handle_callback=*/{});
+
+ // The request is demoted and popup blocked rather than forwarded verbatim,
+ // so the tab is neither navigated nor replaced.
+ EXPECT_EQ(1u, popup_blocker->GetBlockedPopupsCount());
+ EXPECT_EQ(initial_tab_count, browser()->GetTabStripModel()->count());
+ EXPECT_FALSE(active_tab->GetController().GetPendingEntry());
+ EXPECT_EQ(initial_active_url, active_tab->GetLastCommittedURL());
+}
+
+// Dispositions that `blocked_content::ConsiderForPopupBlocking()` ignores must
+// not be forwarded as-is, otherwise the results frame could start downloads or
+// open off-the-record windows without a user gesture.
+IN_PROC_BROWSER_TEST_F(
+ LensOverlayControllerBrowserTest,
+ SidePanel_OpenURLFromTab_DemotesDispositionsThatSkipPopupBlocking) {
+ WaitForPaint();
+
+ content::RenderFrameHost* results_frame = OpenSidePanelAndGetResultsFrame();
+ ASSERT_TRUE(results_frame);
+ content::WebContents* side_panel_contents =
+ GetLensOverlayController()->GetSidePanelWebContentsForTesting();
+ ASSERT_TRUE(side_panel_contents);
+
+ content::WebContents* active_tab =
+ browser()->GetTabStripModel()->GetActiveWebContents();
+ const GURL initial_active_url = active_tab->GetLastCommittedURL();
+ const int initial_tab_count = browser()->GetTabStripModel()->count();
+ auto* popup_blocker =
+ blocked_content::PopupBlockerTabHelper::FromWebContents(active_tab);
+ ASSERT_TRUE(popup_blocker);
+
+ const WindowOpenDisposition kDispositions[] = {
+ WindowOpenDisposition::SAVE_TO_DISK,
+ WindowOpenDisposition::OFF_THE_RECORD,
+ WindowOpenDisposition::CURRENT_TAB,
+ WindowOpenDisposition::SWITCH_TO_TAB,
+ WindowOpenDisposition::NEW_PICTURE_IN_PICTURE,
+ };
+
+ size_t expected_blocked_popups = 0;
+ for (WindowOpenDisposition disposition : kDispositions) {
+ SCOPED_TRACE(testing::Message()
+ << "disposition " << static_cast<int>(disposition));
+ const content::OpenURLParams params = MakeRendererInitiatedParams(
+ results_frame, embedded_test_server()->GetURL("/title1.html"),
+ disposition, /*user_gesture=*/true);
+
+ side_panel_contents->OpenURL(params, /*navigation_handle_callback=*/{});
+
+ // Each request is demoted to NEW_FOREGROUND_TAB, and because there is no
+ // real transient activation it is then caught by the popup blocker.
+ ++expected_blocked_popups;
+ EXPECT_EQ(expected_blocked_popups, popup_blocker->GetBlockedPopupsCount());
+ EXPECT_EQ(initial_tab_count, browser()->GetTabStripModel()->count());
+ EXPECT_FALSE(active_tab->GetController().GetPendingEntry());
+ EXPECT_EQ(initial_active_url, active_tab->GetLastCommittedURL());
+ }
+}
+
+IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest,
+ SidePanel_OpenURLFromTab_BlocksNonWebSchemes) {
+ WaitForPaint();
+
+ content::RenderFrameHost* results_frame = OpenSidePanelAndGetResultsFrame();
+ ASSERT_TRUE(results_frame);
+ content::WebContents* side_panel_contents =
+ GetLensOverlayController()->GetSidePanelWebContentsForTesting();
+ ASSERT_TRUE(side_panel_contents);
+
+ content::WebContents* active_tab =
+ browser()->GetTabStripModel()->GetActiveWebContents();
+ const GURL initial_active_url = active_tab->GetLastCommittedURL();
+ const GURL initial_side_panel_url =
+ side_panel_contents->GetLastCommittedURL();
+ const int initial_tab_count = browser()->GetTabStripModel()->count();
+ auto* popup_blocker =
+ blocked_content::PopupBlockerTabHelper::FromWebContents(active_tab);
... (truncated)
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.
References
On This Page