CVE-2026-113202
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifextensions/browser/guest_view/web_view/web_view_guest.cc |
modified | |
GetPendingWindowOpenURLParamsForTestingextensions/browser/guest_view/web_view/web_view_guest.cc |
modified | |
IN_PROC_BROWSER_TEST_Pchrome/browser/apps/guest_view/web_view_browsertest.cc |
modified |
Files Changed
extensions/browser/guest_view/web_view/web_view_guest.ccchrome/browser/apps/guest_view/web_view_browsertest.ccextensions/browser/guest_view/web_view/web_view_guest.h
Patch
From c771845c7d837119271d139722a01bd4ca2304a4 Mon Sep 17 00:00:00 2001
From: Kevin McNee <mcnee@chromium.org>
Date: Fri, 18 Sep 2026 17:35:19 -0700
Subject: [PATCH] Propagate initiator params in WebViewGuest::OpenURLFromTab
In the WindowOpenDisposition::CURRENT_TAB path, information about the
initiator is dropped, causing the navigation to be misclassified as
browser initiated. We now pass through the provided params.
The linked report also claims, but does not demonstrate, that the new
window path could lead to a misclassified navigation. It's unclear
whether this is possible, but simply passing through the params
regardless is straightforward enough.
We also add test assertions to existing new window tests to confirm
that initiator info is set for well behaved renderers. These pass
without the other changes in this CL.
Fixed: 523201686
Change-Id: Ic08d47a459a12d6a3ff17a1ce7da2f33e55288b9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8399528
Commit-Queue: Kevin McNee <mcnee@chromium.org>
Auto-Submit: Kevin McNee <mcnee@chromium.org>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1701251}
---
diff --git a/extensions/browser/guest_view/web_view/web_view_guest.cc b/extensions/browser/guest_view/web_view/web_view_guest.cc
index 11abf755..36391d1d7 100644
--- a/extensions/browser/guest_view/web_view/web_view_guest.cc
+++ b/extensions/browser/guest_view/web_view/web_view_guest.cc
@@ -33,10 +33,12 @@
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/child_process_security_policy.h"
+#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/navigation_throttle.h"
#include "content/public/browser/navigation_throttle_registry.h"
+#include "content/public/browser/page_navigator.h"
#include "content/public/browser/permission_result.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
@@ -271,6 +273,10 @@
const std::string& name)
: name(name), url(url) {}
+WebViewGuest::NewWindowInfo::NewWindowInfo(const content::OpenURLParams& params,
+ const std::string& name)
+ : name(name), url(params.url), open_url_params(params) {}
+
WebViewGuest::NewWindowInfo::NewWindowInfo(const WebViewGuest::NewWindowInfo&) =
default;
@@ -626,12 +632,15 @@
// We'll need to trigger the intended navigation in the new guest contents,
// but we need to wait until later in the attachment process, after the state
// related to the WebRequest API is set up.
- recreate_initial_nav_ = base::BindOnce(
- &WebViewGuest::LoadURLWithParams, weak_ptr_factory_.GetWeakPtr(),
- web_contents_create_params.initial_popup_url, content::Referrer(),
- ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
- base::OnceCallback<void(content::NavigationHandle&)>(),
- /*force_navigation=*/true);
+ content::NavigationController::LoadURLParams load_url_params(
+ web_contents_create_params.initial_popup_url);
+ load_url_params.referrer = content::Referrer();
+ load_url_params.transition_type = ui::PAGE_TRANSITION_AUTO_TOPLEVEL;
+ recreate_initial_nav_ =
+ base::BindOnce(&WebViewGuest::LoadURLWithParams,
+ weak_ptr_factory_.GetWeakPtr(), std::move(load_url_params),
+ base::OnceCallback<void(content::NavigationHandle&)>(),
+ /*force_navigation=*/true);
}
void WebViewGuest::ClearCodeCache(base::Time remove_since,
@@ -860,7 +869,7 @@
WebViewGuest::Type, owner_rfh(), site_instance, create_params,
base::BindOnce(&WebViewGuest::NewGuestWebViewCallback,
weak_ptr_factory_.GetWeakPtr(), disposition, url,
- main_frame_name));
+ main_frame_name, std::nullopt));
WebViewGuest* guest =
static_cast<WebViewGuest*>(guest_manager->GetGuestByInstanceIDSafely(
guest_instance_id,
@@ -940,7 +949,7 @@
WebViewGuest::Type, embedder_rfh(), nullptr, create_params,
base::BindOnce(&WebViewGuest::NewGuestWebViewCallback,
weak_ptr_factory_.GetWeakPtr(), params.disposition,
- params.url, std::string()));
+ params.url, std::string(), params));
WebViewGuest* guest =
static_cast<WebViewGuest*>(guest_manager->GetGuestByInstanceIDSafely(
guest_instance_id,
@@ -959,14 +968,16 @@
WindowOpenDisposition disposition,
const GURL& url,
const std::string& frame_name,
+ std::optional<content::OpenURLParams> open_url_params,
std::unique_ptr<GuestViewBase> guest) {
auto* raw_new_guest = static_cast<WebViewGuest*>(guest.release());
std::unique_ptr<WebViewGuest> new_guest = base::WrapUnique(raw_new_guest);
raw_new_guest->SetOpener(this);
- pending_new_windows_.insert(
- std::make_pair(raw_new_guest, NewWindowInfo(url, frame_name)));
+ NewWindowInfo info(url, frame_name);
+ info.open_url_params = std::move(open_url_params);
+ pending_new_windows_.insert(std::make_pair(raw_new_guest, std::move(info)));
// Request permission to show the new window.
RequestNewWindowPermission(disposition, gfx::Rect(), std::move(new_guest));
@@ -1699,18 +1710,21 @@
}
GURL url = ResolveURL(src);
+ content::NavigationController::LoadURLParams load_url_params(url);
+ load_url_params.referrer = content::Referrer();
+ load_url_params.transition_type = ui::PAGE_TRANSITION_AUTO_TOPLEVEL;
// We wait for all the content scripts to load and then navigate the guest
// if the navigation is embedder-initiated. For browser-initiated navigations,
// content scripts will be ready.
if (force_navigation) {
SignalWhenReady(base::BindOnce(
- &WebViewGuest::LoadURLWithParams, weak_ptr_factory_.GetWeakPtr(), url,
- content::Referrer(), ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
- std::move(navigation_handle_callback), force_navigation));
+ &WebViewGuest::LoadURLWithParams, weak_ptr_factory_.GetWeakPtr(),
+ std::move(load_url_params), std::move(navigation_handle_callback),
+ force_navigation));
return;
}
- LoadURLWithParams(url, content::Referrer(), ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
+ LoadURLWithParams(std::move(load_url_params),
std::move(navigation_handle_callback), force_navigation);
}
@@ -1812,9 +1826,16 @@
const NewWindowInfo& new_window_info = it->second;
if (!new_window_info.did_start_navigating_away_from_initial_url &&
(new_window_info.url_changed_via_open_url || !HasOpener())) {
- NavigateGuest(new_window_info.url.spec(),
- /*navigation_handle_callback=*/{},
- false /* force_navigation */);
+ if (new_window_info.open_url_params) {
+ LoadURLWithParams(content::NavigationController::LoadURLParams(
+ *new_window_info.open_url_params),
+ /*navigation_handle_callback=*/{},
+ /*force_navigation=*/false);
+ } else {
+ NavigateGuest(new_window_info.url.spec(),
+ /*navigation_handle_callback=*/{},
+ false /* force_navigation */);
+ }
}
// Once a new guest is attached to the DOM of the embedder page, then the
@@ -2027,10 +2048,10 @@
const NewWindowInfo& info = it->second;
// TODO(https://crbug.com/40275094): Consider plumbing
// `navigation_handle_callback`.
- NewWindowInfo new_window_info(params.url, info.name);
+ NewWindowInfo new_window_info(params, info.name);
new_window_info.url_changed_via_open_url =
new_window_info.url != info.url;
- it->second = new_window_info;
+ it->second = std::move(new_window_info);
return nullptr;
}
}
@@ -2044,7 +2065,7 @@
// it is not allowed to navigate to, a 'loadabort' event will fire in the
// embedder, and the guest will be navigated to about:blank.
if (params.disposition == WindowOpenDisposition::CURRENT_TAB) {
- LoadURLWithParams(params.url, params.referrer, params.transition,
+ LoadURLWithParams(content::NavigationController::LoadURLParams(params),
std::move(navigation_handle_callback),
true /* force_navigation */);
return web_contents();
@@ -2134,20 +2155,20 @@
}
void WebViewGuest::LoadURLWithParams(
- const GURL& url,
- const content::Referrer& referrer,
- ui::PageTransition transition_type,
+ content::NavigationController::LoadURLParams load_url_params,
base::OnceCallback<void(content::NavigationHandle&)>
navigation_handle_callback,
bool force_navigation) {
if (!attached() && base::FeatureList::IsEnabled(features::kGuestViewMPArch)) {
pending_first_navigation_ =
- base::BindOnce(&WebViewGuest::LoadURLWithParams, GetWeakPtr(), url,
- referrer, transition_type,
+ base::BindOnce(&WebViewGuest::LoadURLWithParams, GetWeakPtr(),
+ std::move(load_url_params),
std::move(navigation_handle_callback), force_navigation);
return;
}
+ const GURL url = load_url_params.url;
+
if (!url.is_valid()) {
LoadAbort(true /* is_top_level */, url, net::ERR_INVALID_URL);
NavigateGuest(url::kAboutBlankURL, std::move(navigation_handle_callback),
@@ -2187,15 +2208,26 @@
}
GURL validated_url(url);
- GetGuestMainFrame()->GetProcess()->FilterURL(false, &validated_url);
// As guests do not swap processes on navigation, only navigations to
// normal web URLs are supported. No protocol handlers are installed for
// other schemes (e.g., WebUI or extensions), and no permissions or bindings
// can be granted to the guest process.
- content::NavigationController::LoadURLParams load_url_params(validated_url);
- load_url_params.referrer = referrer;
- load_url_params.transition_type = transition_type;
- load_url_params.extra_headers = std::string();
+ if (GetGuestMainFrame()->GetProcess()->FilterURL(false, &validated_url) ==
+ content::RenderProcessHost::FilterURLResult::kBlocked) {
+ // If blocked, we mostly don't preserve the navigation params when
+ // navigating to the block URL.
+ content::NavigationController::LoadURLParams blocked_params(validated_url);
+ blocked_params.referrer = load_url_params.referrer;
+ blocked_params.transition_type = load_url_params.transition_type;
+ load_url_params = std::move(blocked_params);
+ } else {
+ load_url_params.url = validated_url;
+ // TODO(mcnee): Before we propagated the full navigation params, we only
+ // considered main frame navigations here. We clear the FTN id to preserve
+ // this behaviour. Should we handle subframes here? Could such a navigation
+ // reach here?
+ load_url_params.frame_tree_node_id = content::FrameTreeNodeId();
+ }
if (is_overriding_user_agent_) {
load_url_params.override_user_agent =
content::NavigationController::UA_OVERRIDE_TRUE;
@@ -2327,4 +2359,10 @@
return web_contents()->HasOpener();
}
+const std::optional<content::OpenURLParams>&
+WebViewGuest::GetPendingWindowOpenURLParamsForTesting() const {
+ CHECK_EQ(pending_new_windows_.size(), 1u);
+ return pending_new_windows_.begin()->second.open_url_params;
+}
+
} // namespace extensions
diff --git a/extensions/browser/guest_view/web_view/web_view_guest.h b/extensions/browser/guest_view/web_view/web_view_guest.h
index 126586d..5c406dd 100644
--- a/extensions/browser/guest_view/web_view/web_view_guest.h
+++ b/extensions/browser/guest_view/web_view/web_view_guest.h
@@ -16,6 +16,8 @@
#include "base/values.h"
#include "components/guest_view/browser/guest_view.h"
#include "content/public/browser/javascript_dialog_manager.h"
+#include "content/public/browser/navigation_controller.h"
+#include "content/public/browser/page_navigator.h"
#include "content/public/browser/permission_result.h"
#include "extensions/browser/guest_view/web_view/javascript_dialog_helper.h"
#include "extensions/browser/guest_view/web_view/web_view_find_helper.h"
@@ -164,6 +166,9 @@
// Returns spatial navigation status.
bool IsSpatialNavigationEnabled() const;
+ const std::optional<content::OpenURLParams>&
+ GetPendingWindowOpenURLParamsForTesting() const;
+
base::WeakPtr<WebViewGuest> GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
@@ -336,18 +341,18 @@
void PushWebViewStateToIOThread(content::RenderFrameHost* guest_host);
- // Loads the `url` provided. `force_navigation` indicates whether to reload
- // the content if the provided `url` matches the current page of the guest.
- // If a `navigation_handle_callback` function is provided, it should be called
- // with the pending navigation (if any) when the navigation handle become
+ // Loads the URL using the parameters provided in `load_url_params`.
+ // `force_navigation` indicates whether to reload the content if the provided
+ // URL matches the current page of the guest. If a
+ // `navigation_handle_callback` function is provided, it should be called
+ // with the pending navigation (if any) when the navigation handle becomes
// available. This allows callers to observe or attach their specific data.
// This function may not be called if the navigation fails for any reason.
- void LoadURLWithParams(const GURL& url,
- const content::Referrer& referrer,
- ui::PageTransition transition_type,
- base::OnceCallback<void(content::NavigationHandle&)>
- navigation_handle_callback,
- bool force_navigation);
+ void LoadURLWithParams(
+ content::NavigationController::LoadURLParams load_url_params,
+ base::OnceCallback<void(content::NavigationHandle&)>
+ navigation_handle_callback,
+ bool force_navigation);
void RequestNewWindowPermission(WindowOpenDisposition disposition,
const gfx::Rect& initial_bounds,
@@ -363,10 +368,12 @@
// Creates a new guest window owned by this WebViewGuest.
void CreateNewGuestWebViewWindow(const content::OpenURLParams& params);
- void NewGuestWebViewCallback(WindowOpenDisposition disposition,
- const GURL& url,
- const std::string& frame_name,
- std::unique_ptr<GuestViewBase> guest);
+ void NewGuestWebViewCallback(
+ WindowOpenDisposition disposition,
+ const GURL& url,
+ const std::string& frame_name,
+ std::optional<content::OpenURLParams> open_url_params,
+ std::unique_ptr<GuestViewBase> guest);
bool HandleKeyboardShortcuts(const input::NativeWebKeyboardEvent& event);
@@ -410,8 +417,9 @@
std::unique_ptr<WebViewGuestDelegate> web_view_guest_delegate_;
- // Tracks the name, and target URL of the new window. Once the first
- // navigation commits, we no longer track this information.
+ // Tracks the name, target URL, and pending OpenURL parameters of the new
+ // window. Once the first navigation commits, we no longer track this
+ // information.
struct NewWindowInfo {
// Name of the new window.
std::string name;
@@ -419,6 +427,9 @@
// Expected initial URL of the new window.
GURL url;
+ // Parameters for the pending navigation, if any.
+ std::optional<content::OpenURLParams> open_url_params;
+
// Whether OpenURL navigation from the newly created GuestView has changed
// `url`. The pending OpenURL navigation needs to be applied after attaching
// the GuestView.
@@ -430,6 +441,8 @@
bool did_start_navigating_away_from_initial_url = false;
NewWindowInfo(const GURL& url, const std::string& name);
+ NewWindowInfo(const content::OpenURLParams& params,
+ const std::string& name);
NewWindowInfo(const NewWindowInfo&);
~NewWindowInfo();
};
diff --git a/chrome/browser/apps/guest_view/web_view_browsertest.cc b/chrome/browser/apps/guest_view/web_view_browsertest.cc
index fb8b4a22..1807172 100644
--- a/chrome/browser/apps/guest_view/web_view_browsertest.cc
+++ b/chrome/browser/apps/guest_view/web_view_browsertest.cc
@@ -1866,7 +1866,17 @@
}
IN_PROC_BROWSER_TEST_P(WebViewNewWindowTest, Shim_TestNewWindow) {
- TestHelper("testNewWindow", "web_view/shim", NEEDS_TEST_SERVER);
+ ASSERT_TRUE(InitializeEmbeddedTestServer());
+ EmbeddedTestServerAcceptConnections();
+
+ GURL newwindow_url = embedded_test_server()->GetURL(
+ "localhost", "/extensions/platform_apps/web_view/shim/empty_guest.html");
+ content::TestNavigationObserver observer(newwindow_url);
+ observer.StartWatchingNewWebContents();
+ observer.set_wait_event(
+ content::TestNavigationObserver::WaitEvent::kNavigationFinished);
+
+ TestHelper("testNewWindow", "web_view/shim", NO_TEST_SERVER);
// The first <webview> tag in the test will run window.open(), which the
// embedder will translate into an injected second <webview> tag. Ensure
@@ -1887,6 +1897,15 @@
guest_instance1->GetSecurityPrincipal().GetStoragePartitionConfig(),
guest_instance2->GetSecurityPrincipal().GetStoragePartitionConfig());
EXPECT_TRUE(guest_instance1->IsRelatedSiteInstance(guest_instance2));
+
+ observer.Wait();
+ EXPECT_TRUE(observer.last_navigation_succeeded());
+ EXPECT_EQ(newwindow_url, observer.last_navigation_url());
+ EXPECT_EQ(guest1->GetLastCommittedOrigin(), observer.last_initiator_origin());
+ EXPECT_EQ(guest1->GetFrameToken(), observer.last_initiator_frame_token());
+ EXPECT_EQ(guest1->GetProcess()->GetID(),
+ observer.last_initiator_process_id());
+ EXPECT_EQ(guest_instance1, observer.last_source_site_instance());
}
IN_PROC_BROWSER_TEST_P(WebViewNewWindowTest, Shim_TestNewWindowTwoListeners) {
@@ -2069,7 +2088,24 @@
}
IN_PROC_BROWSER_TEST_P(WebViewNewWindowTest, NewWindow_Redirect) {
- TestHelper("testNewWindowRedirect", "web_view/newwindow", NEEDS_TEST_SERVER);
+ ASSERT_TRUE(InitializeEmbeddedTestServer());
+ EmbeddedTestServerAcceptConnections();
+
+ GURL newwindow_url = embedded_test_server()->GetURL(
+ "localhost", "/extensions/platform_apps/web_view/newwindow/guest.html");
+ content::TestNavigationObserver nav_observer(newwindow_url);
... (file diff truncated)
Regression Test / PoC
diff --git a/chrome/browser/apps/guest_view/web_view_browsertest.cc b/chrome/browser/apps/guest_view/web_view_browsertest.cc
index fb8b4a22..1807172 100644
--- a/chrome/browser/apps/guest_view/web_view_browsertest.cc
+++ b/chrome/browser/apps/guest_view/web_view_browsertest.cc
@@ -1866,7 +1866,17 @@
}
IN_PROC_BROWSER_TEST_P(WebViewNewWindowTest, Shim_TestNewWindow) {
- TestHelper("testNewWindow", "web_view/shim", NEEDS_TEST_SERVER);
+ ASSERT_TRUE(InitializeEmbeddedTestServer());
+ EmbeddedTestServerAcceptConnections();
+
+ GURL newwindow_url = embedded_test_server()->GetURL(
+ "localhost", "/extensions/platform_apps/web_view/shim/empty_guest.html");
+ content::TestNavigationObserver observer(newwindow_url);
+ observer.StartWatchingNewWebContents();
+ observer.set_wait_event(
+ content::TestNavigationObserver::WaitEvent::kNavigationFinished);
+
+ TestHelper("testNewWindow", "web_view/shim", NO_TEST_SERVER);
// The first <webview> tag in the test will run window.open(), which the
// embedder will translate into an injected second <webview> tag. Ensure
@@ -1887,6 +1897,15 @@
guest_instance1->GetSecurityPrincipal().GetStoragePartitionConfig(),
guest_instance2->GetSecurityPrincipal().GetStoragePartitionConfig());
EXPECT_TRUE(guest_instance1->IsRelatedSiteInstance(guest_instance2));
+
+ observer.Wait();
+ EXPECT_TRUE(observer.last_navigation_succeeded());
+ EXPECT_EQ(newwindow_url, observer.last_navigation_url());
+ EXPECT_EQ(guest1->GetLastCommittedOrigin(), observer.last_initiator_origin());
+ EXPECT_EQ(guest1->GetFrameToken(), observer.last_initiator_frame_token());
+ EXPECT_EQ(guest1->GetProcess()->GetID(),
+ observer.last_initiator_process_id());
+ EXPECT_EQ(guest_instance1, observer.last_source_site_instance());
}
IN_PROC_BROWSER_TEST_P(WebViewNewWindowTest, Shim_TestNewWindowTwoListeners) {
@@ -2069,7 +2088,24 @@
}
IN_PROC_BROWSER_TEST_P(WebViewNewWindowTest, NewWindow_Redirect) {
- TestHelper("testNewWindowRedirect", "web_view/newwindow", NEEDS_TEST_SERVER);
+ ASSERT_TRUE(InitializeEmbeddedTestServer());
+ EmbeddedTestServerAcceptConnections();
+
+ GURL newwindow_url = embedded_test_server()->GetURL(
+ "localhost", "/extensions/platform_apps/web_view/newwindow/guest.html");
+ content::TestNavigationObserver nav_observer(newwindow_url);
+ nav_observer.StartWatchingNewWebContents();
+ nav_observer.set_wait_event(
+ content::TestNavigationObserver::WaitEvent::kNavigationFinished);
+
+ TestHelper("testNewWindowRedirect", "web_view/newwindow", NO_TEST_SERVER);
+
+ nav_observer.Wait();
+ EXPECT_TRUE(nav_observer.last_navigation_succeeded());
+ EXPECT_EQ(newwindow_url, nav_observer.last_navigation_url());
+ EXPECT_TRUE(nav_observer.last_initiator_origin().has_value());
+ EXPECT_TRUE(nav_observer.last_initiator_frame_token().has_value());
+ EXPECT_TRUE(nav_observer.last_initiator_process_id());
}
IN_PROC_BROWSER_TEST_P(WebViewNewWindowTest, NewWindow_Close) {
@@ -3168,6 +3204,84 @@
EXPECT_EQ(test_url, GetGuestRenderFrameHost()->GetLastCommittedURL());
}
+IN_PROC_BROWSER_TEST_P(WebViewTest,
+ OpenURLFromTab_CurrentTab_PreservesInitiator) {
+ LoadAppWithGuest("web_view/simple");
+
+ GURL test_url("http://www.google.com");
+ content::RenderFrameHost* guest_rfh = GetGuestRenderFrameHost();
+ url::Origin initiator_origin = guest_rfh->GetLastCommittedOrigin();
+ blink::LocalFrameToken initiator_frame_token = guest_rfh->GetFrameToken();
+ content::ChildProcessId initiator_process_id =
+ guest_rfh->GetProcess()->GetID();
+ scoped_refptr<content::SiteInstance> source_site_instance =
+ guest_rfh->GetSiteInstance();
+ content::OpenURLParams params(
+ test_url, content::Referrer(), WindowOpenDisposition::CURRENT_TAB,
+ ui::PAGE_TRANSITION_LINK, true /* is_renderer_initiated */);
+ params.initiator_origin = initiator_origin;
+ params.source_render_frame_id = guest_rfh->GetRoutingID();
+ params.source_render_process_id = initiator_process_id.GetUnsafeValue();
+ params.initiator_process_id = initiator_process_id.GetUnsafeValue();
+ params.initiator_frame_token = initiator_frame_token;
+ params.source_site_instance = source_site_instance;
+
+ content::TestNavigationObserver nav_observer(GetGuestWebContents());
+ nav_observer.set_wait_event(
+ content::TestNavigationObserver::WaitEvent::kNavigationFinished);
+ GetGuestWebContents()->OpenURL(params, /*navigation_handle_callback=*/{});
+
+ nav_observer.Wait();
+
+ EXPECT_EQ(test_url, GetGuestRenderFrameHost()->GetLastCommittedURL());
+ EXPECT_EQ(initiator_origin, nav_observer.last_initiator_origin());
+ EXPECT_EQ(initiator_frame_token, nav_observer.last_initiator_frame_token());
+ EXPECT_EQ(initiator_process_id, nav_observer.last_initiator_process_id());
+ EXPECT_EQ(source_site_instance, nav_observer.last_source_site_instance());
+}
+
+// This test was added speculatively in relation to https://crbug.com/523201686
+// We simply test that the initiator info is propagated through OpenURLFromTab.
+// It's unclear whether a navigation like this could actually be performed.
+IN_PROC_BROWSER_TEST_P(WebViewTest,
+ OpenURLFromTab_NewWindow_PreservesInitiator) {
+ LoadAppWithGuest("web_view/simple");
+
+ GURL test_url("http://www.google.com");
+ content::RenderFrameHost* guest_rfh = GetGuestRenderFrameHost();
+ url::Origin initiator_origin = guest_rfh->GetLastCommittedOrigin();
+ blink::LocalFrameToken initiator_frame_token = guest_rfh->GetFrameToken();
+ content::ChildProcessId initiator_process_id =
+ guest_rfh->GetProcess()->GetID();
+ scoped_refptr<content::SiteInstance> source_site_instance =
+ guest_rfh->GetSiteInstance();
+ content::OpenURLParams params(
+ test_url, content::Referrer(), WindowOpenDisposition::NEW_WINDOW,
+ ui::PAGE_TRANSITION_LINK, true /* is_renderer_initiated */);
+ params.initiator_origin = initiator_origin;
+ params.source_render_frame_id = guest_rfh->GetRoutingID();
+ params.source_render_process_id = initiator_process_id.GetUnsafeValue();
+ params.initiator_process_id = initiator_process_id.GetUnsafeValue();
+ params.initiator_frame_token = initiator_frame_token;
+ params.source_site_instance = source_site_instance;
+
+ GetGuestWebContents()->OpenURL(params, /*navigation_handle_callback=*/{});
+
+ extensions::WebViewGuest* guest =
+ extensions::WebViewGuest::FromGuestViewBase(GetGuestView());
+ ASSERT_TRUE(guest);
+ const std::optional<content::OpenURLParams>& pending_params_opt =
+ guest->GetPendingWindowOpenURLParamsForTesting();
+ ASSERT_TRUE(pending_params_opt.has_value());
+ const content::OpenURLParams& pending_params = *pending_params_opt;
+ EXPECT_EQ(test_url, pending_params.url);
+ EXPECT_EQ(initiator_origin, pending_params.initiator_origin);
+ EXPECT_EQ(initiator_frame_token, pending_params.initiator_frame_token);
+ EXPECT_EQ(initiator_process_id.GetUnsafeValue(),
+ pending_params.initiator_process_id);
+ EXPECT_TRUE(pending_params.is_renderer_initiated);
+}
+
IN_PROC_BROWSER_TEST_P(WebViewNewWindowTest, OpenURLFromTab_NewWindow_Abort) {
LoadAppWithGuest("web_view/simple");
Original Bug Report
Navigation Laundering in <webview> via WebViewGuest::OpenURLFromTab
Flapjack, 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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A logic error in WebViewGuest drops the is_renderer_initiated and initiator_origin flags when a <webview> guest initiates certain navigations. This causes the navigation to be incorrectly classified as browser-initiated, granting it elevated trust. An attacker in a compromised guest renderer can exploit this to bypass SameSite=Strict and Sec-Fetch-Site protections, enabling Cross-Site Request Forgery (CSRF).
Affected files:
extensions/browser/guest_view/web_view/web_view_guest.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Summary
A vulnerability in WebViewGuest allows a compromised <webview> guest renderer to perform “navigation laundering.” When a renderer-initiated navigation is processed via WebViewGuest::OpenURLFromTab or CreateNewGuestWebViewWindow, the parameters indicating the navigation’s origin are lost. Consequently, the browser treats the resulting request as a highly-trusted, browser-initiated navigation (equivalent to a user typing a URL into the address bar). This elevated trust bypasses SameSite=Strict cookie policies and sets Fetch Metadata headers to Sec-Fetch-Site: none, facilitating Cross-Site Request Forgery (CSRF) against target origins within the same StoragePartition as the guest.
Vulnerability Details
When a <webview> guest renderer initiates an OpenURL request (e.g., via window.open targeting a remote frame, or a compromised renderer sending a direct Mojo IPC), the request reaches content::RenderFrameHostImpl::OpenURL. The browser securely identifies the source and creates a content::OpenURLParams object with is_renderer_initiated = true and the correct initiator_origin.
The request is eventually delegated to the guest’s handler, WebViewGuest::OpenURLFromTab (extensions/browser/guest_view/web_view/web_view_guest.cc). For navigations with WindowOpenDisposition::CURRENT_TAB, the code routes the request to LoadURLWithParams:
if (params.disposition == WindowOpenDisposition::CURRENT_TAB) {
LoadURLWithParams(params.url, params.referrer, params.transition,
std::move(navigation_handle_callback),
true /* force_navigation */);
return web_contents();
}
Crucially, LoadURLWithParams only extracts the URL, referrer, and transition type. The is_renderer_initiated and initiator_origin fields are discarded.
Inside WebViewGuest::LoadURLWithParams, a new content::NavigationController::LoadURLParams object is constructed:
content::NavigationController::LoadURLParams load_url_params(validated_url);
load_url_params.referrer = referrer;
// ... fields are populated, but initiator state is omitted ...
GetController().LoadURLWithParams(load_url_params);
Because is_renderer_initiated and initiator_origin are not explicitly set on load_url_params, they fall back to their defaults (false and std::nullopt, respectively). The navigation stack (NavigationRequest) then processes this as a browser_initiated navigation.
A similar propagation failure occurs when a new window disposition is used (e.g., via CreateNewGuestWebViewWindow). The target URL is stored in a NewWindowInfo struct that lacks initiator fields. When the new guest attaches, it calls NavigateGuest, which eventually calls LoadURLWithParams, resulting in the same loss of context.
Security Impact
By laundering a renderer-initiated navigation into a browser-initiated one, a compromised renderer can bypass standard web security boundaries:
- SameSite=Strict Cookie Bypass: Because the navigation lacks an initiator origin, the network stack treats it as a same-site context (
net/cookies/cookie_util.cc). The browser will attachSameSite=Strictcookies to the cross-site request, facilitating CSRF against authenticated services used within the<webview>’s storage partition. - Fetch Metadata (
Sec-Fetch-Site) Bypass: The network service generates theSec-Fetch-Siteheader based on the request initiator. Since the initiator is null, it outputsSec-Fetch-Site: none(services/network/sec_header_helpers.cc), blinding server-side CSRF protections that rely on this header to block cross-origin initiators.
Suggested Exploitation Steps
(Note: These are potential steps; our tooling has not executed a working proof of concept)
- An attacker achieves code execution in a
<webview>guest renderer process. - The attacker identifies a target site within the same
StoragePartitionvulnerable to CSRF via GET requests or top-level navigations (protected only bySameSite=Strictcookies orSec-Fetch-Site). - The attacker constructs a
mojom::FrameHost::OpenURLMojo IPC message targeting the vulnerable endpoint, setting the disposition toCURRENT_TAB. - The renderer sends the IPC to the browser process.
- The browser drops the initiator context in
WebViewGuestand issues the request withSameSite=Strictcookies attached andSec-Fetch-Site: none, successfully executing the CSRF payload.
Suggested Fix
Update the signature of WebViewGuest::LoadURLWithParams and WebViewGuest::NavigateGuest (and the intermediate NewWindowInfo struct) to accept the is_renderer_initiated and initiator_origin fields, or ideally, the entire content::OpenURLParams object where applicable. Ensure that when content::NavigationController::LoadURLParams is constructed in LoadURLWithParams, the is_renderer_initiated and initiator_origin fields are explicitly transferred from the originating request.
Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.