CVE-2026-79123
Overview
Background
- NTP Footer
- a Top-Chrome WebUI strip rendered at the bottom of a New Tab Page tab, used to display management/extension notices and to host links the user can click.
- `OpenURLFromTab`
- the
content::WebContentsDelegateentry point Chrome uses to service a navigation request originating from a given sourceWebContents, deciding whichWebContentsactually navigates. - Split view
- a browser mode that shows two tabs side by side, so the “active” tab is not necessarily the tab a given footer is attached to.
- `base::WeakPtr<content::WebContents>`
- a non-owning, auto-nulling handle used here to reference the footer’s attached tab without extending its lifetime.
Root Cause Analysis
The footer WebUI’s link handler NewTabFooterHandler::OpenUrlInCurrentTab performed navigation by calling browser_window->OpenURL(params, ...), which targets the browser window’s currently active tab rather than the specific tab the footer belongs to. In a split view, the footer attached to one tab could therefore drive a navigation in a different (active) tab, violating the invariant that a footer only navigates its own attached tab context.
The fix re-plumbs navigation so the handler calls web_contents_->OpenURL(...) on the footer’s own WebUI WebContents, whose delegate (NewTabFooterWebView) now implements OpenURLFromTab to forward the request to the stored attached_tab_contents_. NewTabFooterWebView::ShowUI was extended to capture a base::WeakPtr to the attached tab’s WebContents at show time, and OpenURLFromTab returns nullptr (dropping the navigation) if that weak pointer has been invalidated. The regression test OpenUrlFromTabInSplit confirms the target URL now commits in the NTP tab and leaves the active split-view tab untouched, and OpenUrlInCurrentTab_InvalidUrl confirms an invalid URL such as https:// is not forwarded.
browser_window->OpenURL) instead of the footer’s attached tab; the fix routes the navigation through the footer WebUI’s own WebContents and a new OpenURLFromTab override bound to a weak pointer of the correct attached tab.Attack Path
- Open a managed/extension NTP in split view
The user has two tabs side by side, one showing an NTP whose footer is shown because a management or extension notice applies (
ShouldShowExtensionFooter/managed check infooter_controller.cc). - Footer is attached to the non-active tab
The footer belongs to the NTP tab, but a different tab is the active
WebContentsof the browser window. - Trigger a footer navigation
Activating a footer link invokes
NewTabFooterHandler::OpenUrlInCurrentTab, which pre-patch calledbrowser_window->OpenURL, resolving to the browser’s active tab rather than the NTP tab. - Navigation lands in the wrong tab The active (unrelated) tab is navigated to the footer-supplied URL, so a navigation is committed in a tab context that did not originate or expect it (cross-tab navigation context confusion).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/new_tab_footer/footer_controller.cc |
modified | |
ifchrome/browser/ui/views/new_tab_footer/footer_web_view.cc |
modified | |
IN_PROC_BROWSER_TEST_Fchrome/browser/ui/views/new_tab_footer/footer_controller_browsertest.cc |
modified | |
MockWebContentsDelegatechrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler_unittest.cc |
modified | |
NewTabFooterHandlerExtensionTestchrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler_unittest.cc |
modified | |
TEST_Fchrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler_unittest.cc |
modified | |
NewTabFooterHandlerEnterpriseTestchrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler_unittest.cc |
modified |
Files Changed
chrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler_unittest.ccchrome/browser/ui/views/new_tab_footer/footer_web_view.hchrome/browser/ui/webui/new_tab_footer/BUILD.gnchrome/browser/ui/views/new_tab_footer/footer_controller.ccchrome/browser/ui/views/new_tab_footer/footer_controller_browsertest.ccchrome/browser/ui/views/new_tab_footer/footer_web_view.ccchrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler.cc
Audit Directions
- Active-tab vs. source-tab navigationAudit other WebUI/embedded UI handlers that call
browser_window->OpenURL,Navigate, or resolve “current tab” from aBrowser/window rather than from their own sourceWebContents, especially any surface that can exist in split view or be attached to a non-active tab. - Delegate routing for embedded WebContentsReview
WebContentsDelegate::OpenURLFromTabimplementations (and their absence) on embedded WebUI views to ensure navigations are routed to the intended target and safely no-op when the targetWebContentshas been destroyed. - Lifetime of cached tab referencesLook for cached raw pointers to attached tabs in UI controllers that should be
WeakPtrs, where a stale reference could redirect or crash on navigation after tab close.
Patch
From 3bad81ace4d4d6d740b724cb56f47c4b414e05fa Mon Sep 17 00:00:00 2001 From: Paul Adedeji <[email protected]> Date: Mon, 06 Jul 2026 17:24:38 -0700 Subject: [PATCH] [ntp footer] Ensure proper navigation context attribution. Passes explicit source `WebContents` parameters during `OpenUrlInCurrentTab` navigations from the NTP footer WebUI. This ensures navigations are properly associated with their attached tab context. Change-Id: Idaf35b2b6e120a0303b6e8190bdf53882ba5297a Bug: b:517395590 Fixed: b:517395590 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8025870 Auto-Submit: Paul Adedeji <[email protected]> Commit-Queue: Riley Tatum <[email protected]> Reviewed-by: Riley Tatum <[email protected]> Cr-Commit-Position: refs/heads/main@{#1657594} --- diff --git a/chrome/browser/ui/views/new_tab_footer/footer_controller.cc b/chrome/browser/ui/views/new_tab_footer/footer_controller.cc index 6bd39ba..89951e2 100644 --- a/chrome/browser/ui/views/new_tab_footer/footer_controller.cc +++ b/chrome/browser/ui/views/new_tab_footer/footer_controller.cc @@ -145,7 +145,7 @@ const bool show_extension = ShouldShowExtensionFooter(url); const bool show = show_managed || show_extension; if (show) { - footer_->ShowUI(load_start_timestamp, url); + footer_->ShowUI(load_start_timestamp, url, web_contents()->GetWeakPtr()); } else { footer_->CloseUI(); } diff --git a/chrome/browser/ui/views/new_tab_footer/footer_web_view.cc b/chrome/browser/ui/views/new_tab_footer/footer_web_view.cc index b43be1c..15246ae 100644 --- a/chrome/browser/ui/views/new_tab_footer/footer_web_view.cc +++ b/chrome/browser/ui/views/new_tab_footer/footer_web_view.cc @@ -33,8 +33,12 @@ contents_wrapper_ = nullptr; } -void NewTabFooterWebView::ShowUI(base::TimeTicks load_start, GURL url) { +void NewTabFooterWebView::ShowUI( + base::TimeTicks load_start, + GURL url, + base::WeakPtr<content::WebContents> attached_tab_contents) { attached_tab_url_ = url; + attached_tab_contents_ = attached_tab_contents; ShowUI(); base::UmaHistogramMediumTimes("NewTabPage.Footer.ShownTime", base::TimeTicks::Now() - load_start); @@ -100,6 +104,18 @@ event, GetFocusManager()); } +content::WebContents* NewTabFooterWebView::OpenURLFromTab( + content::WebContents* source, + const content::OpenURLParams& params, + base::OnceCallback<void(content::NavigationHandle&)> + navigation_handle_callback) { + if (!attached_tab_contents_) { + return nullptr; + } + return attached_tab_contents_->OpenURL(params, + std::move(navigation_handle_callback)); +} + BEGIN_METADATA(NewTabFooterWebView) END_METADATA diff --git a/chrome/browser/ui/views/new_tab_footer/footer_web_view.h b/chrome/browser/ui/views/new_tab_footer/footer_web_view.h index 8b762ba..78bcf66 100644 --- a/chrome/browser/ui/views/new_tab_footer/footer_web_view.h +++ b/chrome/browser/ui/views/new_tab_footer/footer_web_view.h @@ -38,7 +38,9 @@ NewTabFooterWebView& operator=(const NewTabFooterWebView&) = delete; ~NewTabFooterWebView() override; - void ShowUI(base::TimeTicks load_start, GURL url); + void ShowUI(base::TimeTicks load_start, + GURL url, + base::WeakPtr<content::WebContents> attached_tab_contents); // WebUIContentsWrapper::Host: void ShowUI() override; @@ -49,12 +51,18 @@ void HideCustomContextMenu() override; bool HandleKeyboardEvent(content::WebContents* source, const input::NativeWebKeyboardEvent& event) override; + content::WebContents* OpenURLFromTab( + content::WebContents* source, + const content::OpenURLParams& params, + base::OnceCallback<void(content::NavigationHandle&)> + navigation_handle_callback) override; private: // The URL of tab that the footer is attached to. // This URL is updated on navigation or tab change when the footer needs to be // shown. GURL attached_tab_url_; + base::WeakPtr<content::WebContents> attached_tab_contents_; std::unique_ptr<views::MenuRunner> context_menu_runner_; std::unique_ptr<ui::MenuModel> context_menu_model_; // Processes keyboard events not handled by the renderer. diff --git a/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler.cc b/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler.cc index e007288..0bf13c0 100644 --- a/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler.cc +++ b/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler.cc @@ -184,7 +184,7 @@ content::OpenURLParams params(url, content::Referrer(), WindowOpenDisposition::CURRENT_TAB, ui::PAGE_TRANSITION_LINK, false); - browser_window->OpenURL(params, /*navigation_handle_callback=*/{}); + web_contents_->OpenURL(params, /*navigation_handle_callback=*/{}); } std::string NewTabFooterHandler::GetManagementNoticeText() { diff --git a/chrome/browser/ui/views/new_tab_footer/footer_controller_browsertest.cc b/chrome/browser/ui/views/new_tab_footer/footer_controller_browsertest.cc index be73949b..486728db 100644 --- a/chrome/browser/ui/views/new_tab_footer/footer_controller_browsertest.cc +++ b/chrome/browser/ui/views/new_tab_footer/footer_controller_browsertest.cc @@ -30,6 +30,7 @@ #include "components/search/ntp_features.h" #include "content/public/test/browser_test.h" #include "content/public/test/browser_test_utils.h" +#include "content/public/test/test_navigation_observer.h" #include "extensions/common/extension.h" #include "extensions/test/test_extension_dir.h" #include "net/base/url_util.h" @@ -467,3 +468,34 @@ TabCloseTypes::CLOSE_CREATE_HISTORICAL_TAB); EXPECT_FALSE(footer()->GetVisible()); } + +IN_PROC_BROWSER_TEST_F(FooterControllerSplitViewTest, OpenUrlFromTabInSplit) { + auto extension = LoadNtpExtension(); + + tab_strip_model()->ActivateTabAt(0); + NavigateCurrentTab(GURL(extension->url())); + content::WebContents* ntp_tab_contents = + tab_strip_model()->GetWebContentsAt(0); + + tab_strip_model()->ActivateTabAt(1); + content::WebContents* active_tab_contents = + tab_strip_model()->GetActiveWebContents(); + ASSERT_NE(ntp_tab_contents, active_tab_contents); + + auto* ntp_container = BrowserView::GetBrowserViewForBrowser(browser()) + ->GetContentsContainerViewFor(ntp_tab_contents); + ASSERT_TRUE(ntp_container); + auto* ntp_footer = ntp_container->new_tab_footer_view(); + ASSERT_TRUE(ntp_footer); + + const GURL kTargetUrl("https://www.google.com/"); + content::OpenURLParams params(kTargetUrl, content::Referrer(), + WindowOpenDisposition::CURRENT_TAB, + ui::PAGE_TRANSITION_LINK, false); + content::TestNavigationObserver nav_observer(ntp_tab_contents); + ntp_footer->OpenURLFromTab(ntp_footer->GetWebContents(), params, {}); + nav_observer.Wait(); + + EXPECT_EQ(kTargetUrl, ntp_tab_contents->GetLastCommittedURL()); + EXPECT_EQ(GURL(kNonNtpUrl), active_tab_contents->GetLastCommittedURL()); +} diff --git a/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler_unittest.cc b/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler_unittest.cc index 3d8efbe06..1ae6537 100644 --- a/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler_unittest.cc +++ b/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler_unittest.cc @@ -14,13 +14,16 @@ #include "chrome/browser/extensions/extension_url_overrides.h" #include "chrome/browser/search/background/ntp_custom_background_service.h" #include "chrome/browser/search/background/ntp_custom_background_service_factory.h" +#include "chrome/browser/ui/browser_window/test/mock_browser_window_interface.h" #include "chrome/browser/ui/webui/new_tab_footer/mock_new_tab_footer_document.h" #include "chrome/browser/ui/webui/new_tab_footer/new_tab_footer.mojom.h" #include "chrome/browser/ui/webui/top_chrome/top_chrome_web_ui_controller.h" +#include "chrome/browser/ui/webui/webui_embedding_context.h" #include "chrome/common/pref_names.h" #include "chrome/common/webui_url_constants.h" #include "chrome/grit/theme_resources.h" #include "components/themes/ntp_custom_background_service_observer.h" +#include "content/public/browser/web_contents_delegate.h" #include "content/public/test/test_web_ui.h" #include "extensions/browser/extension_registrar.h" #include "extensions/browser/test_extension_registry_observer.h" @@ -73,6 +76,17 @@ MOCK_METHOD(void, AddObserver, (NtpCustomBackgroundServiceObserver*)); }; +class MockWebContentsDelegate : public content::WebContentsDelegate { + public: + MOCK_METHOD(content::WebContents*, + OpenURLFromTab, + (content::WebContents * source, + const content::OpenURLParams& params, + base::OnceCallback<void(content::NavigationHandle&)> + navigation_handle_callback), + (override)); +}; + class NewTabFooterHandlerExtensionTest : public extensions::ExtensionServiceTestBase { public: @@ -282,6 +296,40 @@ testing::Mock::VerifyAndClearExpectations(&document_); } +TEST_F(NewTabFooterHandlerExtensionTest, OpenUrlInCurrentTab) { + testing::NiceMock<MockBrowserWindowInterface> mock_browser_interface; + webui::SetBrowserWindowInterface(web_contents_.get(), + &mock_browser_interface); + + MockWebContentsDelegate mock_delegate; + web_contents_->SetDelegate(&mock_delegate); + + const GURL kTestUrl("https://www.example.com"); + EXPECT_CALL( + mock_delegate, + OpenURLFromTab(web_contents_.get(), + testing::Field(&content::OpenURLParams::url, kTestUrl), + testing::_)) + .WillOnce(testing::Return(nullptr)); + + handler().OpenUrlInCurrentTab(kTestUrl); +} + +TEST_F(NewTabFooterHandlerExtensionTest, OpenUrlInCurrentTab_InvalidUrl) { + testing::NiceMock<MockBrowserWindowInterface> mock_browser_interface; + webui::SetBrowserWindowInterface(web_contents_.get(), + &mock_browser_interface); + + MockWebContentsDelegate mock_delegate; + web_contents_->SetDelegate(&mock_delegate); + + const GURL kInvalidUrl("https://"); + EXPECT_CALL(mock_delegate, OpenURLFromTab(testing::_, testing::_, testing::_)) + .Times(0); + + handler().OpenUrlInCurrentTab(kInvalidUrl); +} + #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) class NewTabFooterHandlerEnterpriseTest : public testing::Test { public: diff --git a/chrome/browser/ui/webui/new_tab_footer/BUILD.gn b/chrome/browser/ui/webui/new_tab_footer/BUILD.gn index d58eaad..1d980eca 100644 --- a/chrome/browser/ui/webui/new_tab_footer/BUILD.gn +++ b/chrome/browser/ui/webui/new_tab_footer/BUILD.gn @@ -101,6 +101,7 @@ deps = [ ":test_support", "//chrome/browser/enterprise/browser_management:management_identity", + "//chrome/browser/ui/webui:webui_util", "//components/themes", ] }
Regression Test / PoC
diff --git a/chrome/browser/ui/views/new_tab_footer/footer_controller_browsertest.cc b/chrome/browser/ui/views/new_tab_footer/footer_controller_browsertest.cc
index be73949b..486728db 100644
--- a/chrome/browser/ui/views/new_tab_footer/footer_controller_browsertest.cc
+++ b/chrome/browser/ui/views/new_tab_footer/footer_controller_browsertest.cc
@@ -30,6 +30,7 @@
#include "components/search/ntp_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
+#include "content/public/test/test_navigation_observer.h"
#include "extensions/common/extension.h"
#include "extensions/test/test_extension_dir.h"
#include "net/base/url_util.h"
@@ -467,3 +468,34 @@
TabCloseTypes::CLOSE_CREATE_HISTORICAL_TAB);
EXPECT_FALSE(footer()->GetVisible());
}
+
+IN_PROC_BROWSER_TEST_F(FooterControllerSplitViewTest, OpenUrlFromTabInSplit) {
+ auto extension = LoadNtpExtension();
+
+ tab_strip_model()->ActivateTabAt(0);
+ NavigateCurrentTab(GURL(extension->url()));
+ content::WebContents* ntp_tab_contents =
+ tab_strip_model()->GetWebContentsAt(0);
+
+ tab_strip_model()->ActivateTabAt(1);
+ content::WebContents* active_tab_contents =
+ tab_strip_model()->GetActiveWebContents();
+ ASSERT_NE(ntp_tab_contents, active_tab_contents);
+
+ auto* ntp_container = BrowserView::GetBrowserViewForBrowser(browser())
+ ->GetContentsContainerViewFor(ntp_tab_contents);
+ ASSERT_TRUE(ntp_container);
+ auto* ntp_footer = ntp_container->new_tab_footer_view();
+ ASSERT_TRUE(ntp_footer);
+
+ const GURL kTargetUrl("https://www.google.com/");
+ content::OpenURLParams params(kTargetUrl, content::Referrer(),
+ WindowOpenDisposition::CURRENT_TAB,
+ ui::PAGE_TRANSITION_LINK, false);
+ content::TestNavigationObserver nav_observer(ntp_tab_contents);
+ ntp_footer->OpenURLFromTab(ntp_footer->GetWebContents(), params, {});
+ nav_observer.Wait();
+
+ EXPECT_EQ(kTargetUrl, ntp_tab_contents->GetLastCommittedURL());
+ EXPECT_EQ(GURL(kNonNtpUrl), active_tab_contents->GetLastCommittedURL());
+}
diff --git a/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler_unittest.cc b/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler_unittest.cc
index 3d8efbe06..1ae6537 100644
--- a/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler_unittest.cc
+++ b/chrome/browser/ui/webui/new_tab_footer/new_tab_footer_handler_unittest.cc
@@ -14,13 +14,16 @@
#include "chrome/browser/extensions/extension_url_overrides.h"
#include "chrome/browser/search/background/ntp_custom_background_service.h"
#include "chrome/browser/search/background/ntp_custom_background_service_factory.h"
+#include "chrome/browser/ui/browser_window/test/mock_browser_window_interface.h"
#include "chrome/browser/ui/webui/new_tab_footer/mock_new_tab_footer_document.h"
#include "chrome/browser/ui/webui/new_tab_footer/new_tab_footer.mojom.h"
#include "chrome/browser/ui/webui/top_chrome/top_chrome_web_ui_controller.h"
+#include "chrome/browser/ui/webui/webui_embedding_context.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/theme_resources.h"
#include "components/themes/ntp_custom_background_service_observer.h"
+#include "content/public/browser/web_contents_delegate.h"
#include "content/public/test/test_web_ui.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/test_extension_registry_observer.h"
@@ -73,6 +76,17 @@
MOCK_METHOD(void, AddObserver, (NtpCustomBackgroundServiceObserver*));
};
+class MockWebContentsDelegate : public content::WebContentsDelegate {
+ public:
+ MOCK_METHOD(content::WebContents*,
+ OpenURLFromTab,
+ (content::WebContents * source,
+ const content::OpenURLParams& params,
+ base::OnceCallback<void(content::NavigationHandle&)>
+ navigation_handle_callback),
+ (override));
+};
+
class NewTabFooterHandlerExtensionTest
: public extensions::ExtensionServiceTestBase {
public:
@@ -282,6 +296,40 @@
testing::Mock::VerifyAndClearExpectations(&document_);
}
+TEST_F(NewTabFooterHandlerExtensionTest, OpenUrlInCurrentTab) {
+ testing::NiceMock<MockBrowserWindowInterface> mock_browser_interface;
+ webui::SetBrowserWindowInterface(web_contents_.get(),
+ &mock_browser_interface);
+
+ MockWebContentsDelegate mock_delegate;
+ web_contents_->SetDelegate(&mock_delegate);
+
+ const GURL kTestUrl("https://www.example.com");
+ EXPECT_CALL(
+ mock_delegate,
+ OpenURLFromTab(web_contents_.get(),
+ testing::Field(&content::OpenURLParams::url, kTestUrl),
+ testing::_))
+ .WillOnce(testing::Return(nullptr));
+
+ handler().OpenUrlInCurrentTab(kTestUrl);
+}
+
+TEST_F(NewTabFooterHandlerExtensionTest, OpenUrlInCurrentTab_InvalidUrl) {
+ testing::NiceMock<MockBrowserWindowInterface> mock_browser_interface;
+ webui::SetBrowserWindowInterface(web_contents_.get(),
+ &mock_browser_interface);
+
+ MockWebContentsDelegate mock_delegate;
+ web_contents_->SetDelegate(&mock_delegate);
+
+ const GURL kInvalidUrl("https://");
+ EXPECT_CALL(mock_delegate, OpenURLFromTab(testing::_, testing::_, testing::_))
+ .Times(0);
+
+ handler().OpenUrlInCurrentTab(kInvalidUrl);
+}
+
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
class NewTabFooterHandlerEnterpriseTest : public testing::Test {
public: