Chrome · BrowserTag
CVE-2026-106425
Authorization Error in BrowserTag
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/renderer_context_menu/render_view_context_menu.cc |
modified | |
ifchrome/browser/download/download_browsertest.cc |
modified | |
ifcontent/public/test/test_utils.cc |
modified |
Files Changed
content/public/test/test_utils.hchrome/browser/download/download_browsertest.cccontent/public/test/test_utils.cccontent/browser/web_contents/web_contents_impl.ccchrome/browser/renderer_context_menu/render_view_context_menu.cc
Patch
From aefc14aff566e33023dc46999df964d627afffb1 Mon Sep 17 00:00:00 2001
From: Kevin McNee <mcnee@chromium.org>
Date: Mon, 14 Sep 2026 14:51:48 -0700
Subject: [PATCH] Ensure PDF Save As initiator change is not applied to other embedders
The PDF viewer requires the download to be initiated as if by its
embedder. This was mistakenly applied to other cases of inner
WebContents if the renderer claimed to be showing a context menu for a
plugin. We now verify the source is a mime handler and add additional
checks in the save frame implementation to guard against crossing
storage partitions when changing the initiator like this.
Fixed: 501790682
Change-Id: Ib16661d9eacd9ddb7d4dd9481c138109567c58f0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8383798
Auto-Submit: Kevin McNee <mcnee@chromium.org>
Commit-Queue: Avi Drissman <avi@chromium.org>
Reviewed-by: Avi Drissman <avi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1697575}
---
diff --git a/chrome/browser/renderer_context_menu/render_view_context_menu.cc b/chrome/browser/renderer_context_menu/render_view_context_menu.cc
index 0389028..5271cc2 100644
--- a/chrome/browser/renderer_context_menu/render_view_context_menu.cc
+++ b/chrome/browser/renderer_context_menu/render_view_context_menu.cc
@@ -5168,9 +5168,11 @@
#endif // BUILDFLAG(ENABLE_PDF)
if (!target_frame_host) {
- target_frame_host = is_plugin
- ? source_web_contents_->GetOuterWebContentsFrame()
- : frame_host;
+ target_frame_host =
+ is_plugin && extensions::MimeHandlerViewGuest::FromRenderFrameHost(
+ frame_host)
+ ? source_web_contents_->GetOuterWebContentsFrame()
+ : frame_host;
if (!target_frame_host) {
return;
}
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index 80170c73..649c13a 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -7258,6 +7258,15 @@
DCHECK(rfh);
auto& rfhi = *static_cast<RenderFrameHostImpl*>(rfh);
+ if (WebContents::FromRenderFrameHost(rfh) != this) {
+ // Note that the PDF viewer can legitimately save from the context of a RFH
+ // outside of this WebContents. The following CHECK guards against the
+ // caller possibly being tricked into providing a RFH with different storage
+ // access. See https://crbug.com/40167434 and https://crbug.com/501790682
+ CHECK_EQ(rfhi.GetStoragePartition(),
+ GetPrimaryMainFrame()->GetStoragePartition());
+ }
+
OPTIONAL_TRACE_EVENT2("content", "WebContentsImpl::SaveFrameWithHeaders",
"url", url, "headers", headers);
// Check and see if the guest can handle this.
diff --git a/chrome/browser/download/download_browsertest.cc b/chrome/browser/download/download_browsertest.cc
index 8e1c020..00857c3 100644
--- a/chrome/browser/download/download_browsertest.cc
+++ b/chrome/browser/download/download_browsertest.cc
@@ -116,6 +116,7 @@
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
+#include "content/public/browser/site_instance.h"
#include "content/public/browser/storage_partition_config.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
@@ -132,6 +133,7 @@
#include "content/public/test/test_download_http_response.h"
#include "content/public/test/test_file_error_injector.h"
#include "content/public/test/test_navigation_observer.h"
+#include "content/public/test/test_utils.h"
#include "content/public/test/url_loader_interceptor.h"
#include "extensions/browser/extension_dialog_auto_confirm.h"
#include "extensions/browser/extension_registry.h"
@@ -2777,6 +2779,91 @@
download_waiter->NumDownloadsSeenInState(DownloadItem::COMPLETE));
}
+// Regression test for https://crbug.com/501790682
+// PDF plugin downloads are treated as initiated by the embedder. This should
+// not apply to other forms of inner WebContents embedding, especially when the
+// embedder uses another StoragePartition.
+IN_PROC_BROWSER_TEST_F(DownloadTest,
+ SpoofedSavePluginFromNonPluginInnerWebContents) {
+ https_test_server()->ServeFilesFromDirectory(GetTestDataDirectory());
+ ASSERT_TRUE(https_test_server()->Start());
+ EnableFileChooser(true);
+
+ const url::Origin inner_origin =
+ url::Origin::Create(https_test_server()->GetURL("b.test", "/"));
+ const url::Origin expected_request_initiator = inner_origin;
+
+ content::WebContents* web_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ const GURL url =
+ https_test_server()->GetURL("a.test", "/iframe_about_blank.html");
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+ // Do a second navigation so that there's an initiator.
+ ASSERT_TRUE(content::NavigateToURLFromRenderer(web_contents, url));
+
+ content::RenderFrameHost* subframe =
+ content::ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0);
+ ASSERT_TRUE(subframe);
+
+ const content::StoragePartitionConfig inner_partition_config =
+ content::StoragePartitionConfig::Create(browser()->GetProfile(), "b.test",
+ "partition",
+ /*in_memory=*/true);
+ scoped_refptr<content::SiteInstance> inner_site_instance =
+ content::SiteInstance::CreateForGuest(browser()->GetProfile(),
+ inner_partition_config);
+ content::WebContents::CreateParams inner_params(browser()->GetProfile(),
+ inner_site_instance);
+ content::WebContents* inner_contents =
+ content::CreateAndAttachInnerContents(subframe, inner_params);
+ ASSERT_TRUE(inner_contents);
+
+ const GURL inner_url = https_test_server()->GetURL("b.test", "/title1.html");
+ ASSERT_TRUE(content::NavigateToURL(inner_contents, inner_url));
+ // Do a second navigation so that there's an initiator to check on the
+ // download.
+ ASSERT_TRUE(content::NavigateToURLFromRenderer(inner_contents, inner_url));
+ ASSERT_EQ(inner_origin,
+ inner_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin());
+
+ const GURL download_url =
+ https_test_server()->GetURL("b.test", "/downloads/image.jpg");
+
+ std::optional<url::Origin> request_initiator;
+ base::RunLoop request_waiter;
+ content::URLLoaderInterceptor request_listener(base::BindLambdaForTesting(
+ [&](content::URLLoaderInterceptor::RequestParams* params) {
+ if (params->url_request.url == download_url) {
+ request_initiator = params->url_request.request_initiator;
+ request_waiter.Quit();
+ }
+ return false;
+ }));
+
+ std::unique_ptr<content::DownloadTestObserver> download_waiter(
+ CreateWaiter(browser(), 1));
+
+ content::ContextMenuParams context_menu_params;
+ context_menu_params.media_type =
+ blink::mojom::ContextMenuDataMediaType::kPlugin;
+ context_menu_params.src_url = download_url;
+ context_menu_params.page_url = inner_contents->GetLastCommittedURL();
+ TestRenderViewContextMenu menu(*inner_contents->GetPrimaryMainFrame(),
+ context_menu_params);
+ menu.Init();
+ menu.ExecuteCommand(IDC_CONTENT_CONTEXT_SAVEPLUGINAS, 0);
+
+ request_waiter.Run();
+
+ ASSERT_TRUE(request_initiator.has_value());
+ EXPECT_EQ(*request_initiator, expected_request_initiator);
+
+ download_waiter->WaitForFinished();
+ EXPECT_EQ(1u,
+ download_waiter->NumDownloadsSeenInState(DownloadItem::COMPLETE));
+}
+
#if BUILDFLAG(ENABLE_PDF)
IN_PROC_BROWSER_TEST_P(PdfDownloadTestSplitCacheEnabled,
SaveSubframePdfFromContextMenuIsolationInfo) {
diff --git a/content/public/test/test_utils.cc b/content/public/test/test_utils.cc
index f512fb2d..d9d7add 100644
--- a/content/public/test/test_utils.cc
+++ b/content/public/test/test_utils.cc
@@ -258,12 +258,19 @@
host;
}
-WebContents* CreateAndAttachInnerContents(RenderFrameHost* rfh) {
+WebContents* CreateAndAttachInnerContents(
+ RenderFrameHost* rfh,
+ std::optional<WebContents::CreateParams> create_params) {
auto* outer_contents = WebContents::FromRenderFrameHost(rfh);
- if (!outer_contents)
+ if (!outer_contents) {
return nullptr;
+ }
- WebContents::CreateParams inner_params(outer_contents->GetBrowserContext());
+ CHECK(!create_params ||
+ create_params->browser_context == outer_contents->GetBrowserContext());
+
+ WebContents::CreateParams inner_params = create_params.value_or(
+ WebContents::CreateParams(outer_contents->GetBrowserContext()));
std::unique_ptr<WebContents> inner_contents_ptr =
WebContents::Create(inner_params);
diff --git a/content/public/test/test_utils.h b/content/public/test/test_utils.h
index 4e7385f..d9eade8 100644
--- a/content/public/test/test_utils.h
+++ b/content/public/test/test_utils.h
@@ -6,6 +6,7 @@
#define CONTENT_PUBLIC_TEST_TEST_UTILS_H_
#include <memory>
+#include <optional>
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
@@ -17,6 +18,7 @@
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/global_routing_id.h"
+#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "third_party/blink/public/common/fetch/fetch_api_request_headers_map.h"
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-forward.h"
@@ -146,7 +148,9 @@
// Returns a pointer to the inner WebContents, which is now owned by the outer
// WebContents. The caller should be careful when retaining the pointer, as the
// inner WebContents will be deleted if the frame it's attached to goes away.
-WebContents* CreateAndAttachInnerContents(RenderFrameHost* rfh);
+WebContents* CreateAndAttachInnerContents(
+ RenderFrameHost* rfh,
+ std::optional<WebContents::CreateParams> create_params = std::nullopt);
// Spins a run loop until IsDocumentOnLoadCompletedInPrimaryMainFrame() is true.
void AwaitDocumentOnLoadCompleted(WebContents* web_contents);
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/download/download_browsertest.cc b/chrome/browser/download/download_browsertest.cc
index 8e1c020..00857c3 100644
--- a/chrome/browser/download/download_browsertest.cc
+++ b/chrome/browser/download/download_browsertest.cc
@@ -116,6 +116,7 @@
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
+#include "content/public/browser/site_instance.h"
#include "content/public/browser/storage_partition_config.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
@@ -132,6 +133,7 @@
#include "content/public/test/test_download_http_response.h"
#include "content/public/test/test_file_error_injector.h"
#include "content/public/test/test_navigation_observer.h"
+#include "content/public/test/test_utils.h"
#include "content/public/test/url_loader_interceptor.h"
#include "extensions/browser/extension_dialog_auto_confirm.h"
#include "extensions/browser/extension_registry.h"
@@ -2777,6 +2779,91 @@
download_waiter->NumDownloadsSeenInState(DownloadItem::COMPLETE));
}
+// Regression test for https://crbug.com/501790682
+// PDF plugin downloads are treated as initiated by the embedder. This should
+// not apply to other forms of inner WebContents embedding, especially when the
+// embedder uses another StoragePartition.
+IN_PROC_BROWSER_TEST_F(DownloadTest,
+ SpoofedSavePluginFromNonPluginInnerWebContents) {
+ https_test_server()->ServeFilesFromDirectory(GetTestDataDirectory());
+ ASSERT_TRUE(https_test_server()->Start());
+ EnableFileChooser(true);
+
+ const url::Origin inner_origin =
+ url::Origin::Create(https_test_server()->GetURL("b.test", "/"));
+ const url::Origin expected_request_initiator = inner_origin;
+
+ content::WebContents* web_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ const GURL url =
+ https_test_server()->GetURL("a.test", "/iframe_about_blank.html");
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+ // Do a second navigation so that there's an initiator.
+ ASSERT_TRUE(content::NavigateToURLFromRenderer(web_contents, url));
+
+ content::RenderFrameHost* subframe =
+ content::ChildFrameAt(web_contents->GetPrimaryMainFrame(), 0);
+ ASSERT_TRUE(subframe);
+
+ const content::StoragePartitionConfig inner_partition_config =
+ content::StoragePartitionConfig::Create(browser()->GetProfile(), "b.test",
+ "partition",
+ /*in_memory=*/true);
+ scoped_refptr<content::SiteInstance> inner_site_instance =
+ content::SiteInstance::CreateForGuest(browser()->GetProfile(),
+ inner_partition_config);
+ content::WebContents::CreateParams inner_params(browser()->GetProfile(),
+ inner_site_instance);
+ content::WebContents* inner_contents =
+ content::CreateAndAttachInnerContents(subframe, inner_params);
+ ASSERT_TRUE(inner_contents);
+
+ const GURL inner_url = https_test_server()->GetURL("b.test", "/title1.html");
+ ASSERT_TRUE(content::NavigateToURL(inner_contents, inner_url));
+ // Do a second navigation so that there's an initiator to check on the
+ // download.
+ ASSERT_TRUE(content::NavigateToURLFromRenderer(inner_contents, inner_url));
+ ASSERT_EQ(inner_origin,
+ inner_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin());
+
+ const GURL download_url =
+ https_test_server()->GetURL("b.test", "/downloads/image.jpg");
+
+ std::optional<url::Origin> request_initiator;
+ base::RunLoop request_waiter;
+ content::URLLoaderInterceptor request_listener(base::BindLambdaForTesting(
+ [&](content::URLLoaderInterceptor::RequestParams* params) {
+ if (params->url_request.url == download_url) {
+ request_initiator = params->url_request.request_initiator;
+ request_waiter.Quit();
+ }
+ return false;
+ }));
+
+ std::unique_ptr<content::DownloadTestObserver> download_waiter(
+ CreateWaiter(browser(), 1));
+
+ content::ContextMenuParams context_menu_params;
+ context_menu_params.media_type =
+ blink::mojom::ContextMenuDataMediaType::kPlugin;
+ context_menu_params.src_url = download_url;
+ context_menu_params.page_url = inner_contents->GetLastCommittedURL();
+ TestRenderViewContextMenu menu(*inner_contents->GetPrimaryMainFrame(),
+ context_menu_params);
+ menu.Init();
+ menu.ExecuteCommand(IDC_CONTENT_CONTEXT_SAVEPLUGINAS, 0);
+
+ request_waiter.Run();
+
+ ASSERT_TRUE(request_initiator.has_value());
+ EXPECT_EQ(*request_initiator, expected_request_initiator);
+
+ download_waiter->WaitForFinished();
+ EXPECT_EQ(1u,
+ download_waiter->NumDownloadsSeenInState(DownloadItem::COMPLETE));
+}
+
#if BUILDFLAG(ENABLE_PDF)
IN_PROC_BROWSER_TEST_P(PdfDownloadTestSplitCacheEnabled,
SaveSubframePdfFromContextMenuIsolationInfo) {
diff --git a/content/public/test/test_utils.cc b/content/public/test/test_utils.cc
index f512fb2d..d9d7add 100644
--- a/content/public/test/test_utils.cc
+++ b/content/public/test/test_utils.cc
@@ -258,12 +258,19 @@
host;
}
-WebContents* CreateAndAttachInnerContents(RenderFrameHost* rfh) {
+WebContents* CreateAndAttachInnerContents(
+ RenderFrameHost* rfh,
+ std::optional<WebContents::CreateParams> create_params) {
auto* outer_contents = WebContents::FromRenderFrameHost(rfh);
- if (!outer_contents)
+ if (!outer_contents) {
return nullptr;
+ }
- WebContents::CreateParams inner_params(outer_contents->GetBrowserContext());
+ CHECK(!create_params ||
+ create_params->browser_context == outer_contents->GetBrowserContext());
+
+ WebContents::CreateParams inner_params = create_params.value_or(
+ WebContents::CreateParams(outer_contents->GetBrowserContext()));
std::unique_ptr<WebContents> inner_contents_ptr =
WebContents::Create(inner_params);
diff --git a/content/public/test/test_utils.h b/content/public/test/test_utils.h
index 4e7385f..d9eade8 100644
--- a/content/public/test/test_utils.h
+++ b/content/public/test/test_utils.h
@@ -6,6 +6,7 @@
#define CONTENT_PUBLIC_TEST_TEST_UTILS_H_
#include <memory>
+#include <optional>
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
@@ -17,6 +18,7 @@
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/global_routing_id.h"
+#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "third_party/blink/public/common/fetch/fetch_api_request_headers_map.h"
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-forward.h"
@@ -146,7 +148,9 @@
// Returns a pointer to the inner WebContents, which is now owned by the outer
// WebContents. The caller should be careful when retaining the pointer, as the
// inner WebContents will be deleted if the frame it's attached to goes away.
-WebContents* CreateAndAttachInnerContents(RenderFrameHost* rfh);
+WebContents* CreateAndAttachInnerContents(
+ RenderFrameHost* rfh,
+ std::optional<WebContents::CreateParams> create_params = std::nullopt);
// Spins a run loop until IsDocumentOnLoadCompletedInPrimaryMainFrame() is true.
void AwaitDocumentOnLoadCompleted(WebContents* web_contents);
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