CVE-2026-9980
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/printing/print_browsertest.cc |
modified | |
IN_PROC_BROWSER_TEST_Fchrome/browser/printing/print_browsertest.cc |
modified | |
ifchrome/browser/printing/print_view_manager.cc |
modified |
Files Changed
chrome/browser/bad_message.hchrome/browser/printing/print_browsertest.ccchrome/browser/printing/print_view_manager.cctools/metrics/histograms/metadata/stability/enums.xml
Patch
From 07e3684d85aa4458fcad1d96dd969594793beb7d Mon Sep 17 00:00:00 2001 From: Lei Zhang <[email protected]> Date: Mon, 18 May 2026 16:04:39 -0700 Subject: [PATCH] Printing: Reject Fenced Frame requests in PrintViewManager Add a missing check to PrintViewManager::PrintPreview() to make sure the browser does not initiate printing for a Fenced Frame. Verify this with a new PrintFencedFrameBrowserTest case. Add a check for Fenced Frame in PrintViewManager::RequestPrintPreview() to verify the renderer's reply IPC as well, and use ReceivedBadMessage() to terminate the bad renderer. This should not happen in practice, except possibly in a compromised renderer. Add a new BadMessageReasonChrome enum value for this case, to distinguish it from the bad messages that can be triggered by window.print(). Tidy PrintFencedFrameBrowserTest along the way. Bug: 511776372 Change-Id: Ibefa88901306b00d535024612ce25394e9a6dfab Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7852530 Reviewed-by: Tsuyoshi Horo <[email protected]> Reviewed-by: Mark Pearson <[email protected]> Commit-Queue: Lei Zhang <[email protected]> Cr-Commit-Position: refs/heads/main@{#1632454} --- diff --git a/chrome/browser/bad_message.h b/chrome/browser/bad_message.h index e7f5d6a..449cc96 100644 --- a/chrome/browser/bad_message.h +++ b/chrome/browser/bad_message.h @@ -32,6 +32,7 @@ SSI_CREATE_FENCED_FRAME = 9, CCU_SUPERFLUOUS_BIND = 10, RFH_INVALID_WEB_FRAME_URL = 11, + PVM_PRINT_FENCED_FRAME = 12, // Please add new elements here. The naming convention is abbreviated class // name (e.g. RenderFrameHost becomes RFH) plus a unique description of the diff --git a/chrome/browser/printing/print_browsertest.cc b/chrome/browser/printing/print_browsertest.cc index 625b49c..f996b75 100644 --- a/chrome/browser/printing/print_browsertest.cc +++ b/chrome/browser/printing/print_browsertest.cc @@ -2070,8 +2070,9 @@ content::RenderFrameHost* CreateFencedFrame( content::RenderFrameHost* fenced_frame_parent, const GURL& url) { - if (fenced_frame_helper_) + if (fenced_frame_helper_) { return fenced_frame_helper_->CreateFencedFrame(fenced_frame_parent, url); + } // FencedFrameTestHelper only supports the MPArch version of fenced frames. // So need to maually create a fenced frame for the ShadowDOM version. @@ -2085,12 +2086,10 @@ content::JsReplace(kAddFencedFrameScript, url))); EXPECT_TRUE(navigation.WaitForNavigationFinished()); - content::RenderFrameHost* new_frame = ChildFrameAt(fenced_frame_parent, 0); - - return new_frame; + return ChildFrameAt(fenced_frame_parent, 0); } - void RunPrintTest(const std::string& print_command) { + void RunScriptedPrintTest(const std::string& print_command) { // Navigate to an initial page. const GURL url(https_server_.GetURL("/empty.html")); ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); @@ -2122,9 +2121,9 @@ )"; const std::string test_script = base::StringPrintf(kAddListenersScript, print_command.c_str()); - EXPECT_EQ("beforeprint: false, afterprint: false", content::EvalJs(fenced_frame_host, test_script)); + ASSERT_TRUE(console_observer.Wait()); ASSERT_EQ(1u, console_observer.messages().size()); EXPECT_EQ( @@ -2132,18 +2131,41 @@ console_observer.GetMessageAt(0)); } + void RunPrintTest() { + // Navigate to an initial page. + const GURL url(https_server_.GetURL("/empty.html")); + ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); + + // Load a fenced frame. + GURL fenced_frame_url = https_server_.GetURL("/fenced_frames/title1.html"); + content::WebContents* web_contents = + browser()->tab_strip_model()->GetActiveWebContents(); + content::RenderFrameHost* fenced_frame_host = CreateFencedFrame( + web_contents->GetPrimaryMainFrame(), fenced_frame_url); + ASSERT_TRUE(fenced_frame_host); + + // `PrintViewManager` should refuse to print. + auto* print_view_manager = PrintViewManager::FromWebContents(web_contents); + ASSERT_TRUE(print_view_manager); + EXPECT_FALSE(print_view_manager->PrintPreviewNow(fenced_frame_host, + /*has_selection=*/false)); + } + private: - base::test::ScopedFeatureList feature_list_; std::unique_ptr<content::test::FencedFrameTestHelper> fenced_frame_helper_; net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS}; }; IN_PROC_BROWSER_TEST_F(PrintFencedFrameBrowserTest, ScriptedPrint) { - RunPrintTest("window.print();"); + RunScriptedPrintTest("window.print();"); } IN_PROC_BROWSER_TEST_F(PrintFencedFrameBrowserTest, DocumentExecCommand) { - RunPrintTest("document.execCommand('print');"); + RunScriptedPrintTest("document.execCommand('print');"); +} + +IN_PROC_BROWSER_TEST_F(PrintFencedFrameBrowserTest, BrowserPrint) { + RunPrintTest(); } #if BUILDFLAG(IS_WIN) diff --git a/chrome/browser/printing/print_view_manager.cc b/chrome/browser/printing/print_view_manager.cc index c886d0c..aa8dc760 100644 --- a/chrome/browser/printing/print_view_manager.cc +++ b/chrome/browser/printing/print_view_manager.cc @@ -286,12 +286,19 @@ // Users can send print commands all they want and it is beyond // PrintViewManager's control. Just ignore the extra commands. // See http://crbug.com/40240300 for example. - if (print_preview_state_ != NOT_PREVIEWING) + if (print_preview_state_ != NOT_PREVIEWING) { return false; + } // Don't print / print preview crashed tabs. - if (IsCrashed() || !rfh->IsRenderFrameLive()) + if (IsCrashed() || !rfh->IsRenderFrameLive()) { return false; + } + + // Don't print / print preview fenched frames. + if (rfh->IsNestedWithinFencedFrame()) { + return false; + } GetPrintRenderFrame(rfh)->InitiatePrintPreview( #if BUILDFLAG(IS_CHROMEOS) @@ -426,11 +433,22 @@ void PrintViewManager::RequestPrintPreview( mojom::RequestPrintPreviewParamsPtr params) { + auto* rfh = GetCurrentTargetFrame(); + if (rfh->IsNestedWithinFencedFrame()) { + // Either the renderer should have checked and disallowed the request for + // fenced frames in ChromeClient, or PrintPreview() above should have + // checked. Ignore the request and mark it as bad if those checks didn't + // happen for some reason. + bad_message::ReceivedBadMessage(rfh->GetProcess(), + bad_message::PVM_PRINT_FENCED_FRAME); + return; + } + #if BUILDFLAG(ENTERPRISE_CONTENT_ANALYSIS) set_analyzing_content(/*analyzing=*/true); #endif RejectPrintPreviewRequestIfRestricted( - GetCurrentTargetFrame()->GetGlobalId(), + rfh->GetGlobalId(), base::BindOnce(&PrintViewManager::OnRequestPrintPreviewCallback, weak_factory_.GetWeakPtr(), std::move(params), GetCurrentTargetFrame()->GetGlobalId())); diff --git a/tools/metrics/histograms/metadata/stability/enums.xml b/tools/metrics/histograms/metadata/stability/enums.xml index 6f859cbb..6bc70699 100644 --- a/tools/metrics/histograms/metadata/stability/enums.xml +++ b/tools/metrics/histograms/metadata/stability/enums.xml @@ -129,6 +129,7 @@ <int value="9" label="SSI_CREATE_FENCED_FRAME"/> <int value="10" label="CCU_SUPERFLUOUS_BIND"/> <int value="11" label="RFH_INVALID_WEB_FRAME_URL"/> + <int value="12" label="PVM_PRINT_FENCED_FRAME"/> </enum> <enum name="BadMessageReasonContent">
Regression Test / PoC
diff --git a/chrome/browser/printing/print_browsertest.cc b/chrome/browser/printing/print_browsertest.cc
index 625b49c..f996b75 100644
--- a/chrome/browser/printing/print_browsertest.cc
+++ b/chrome/browser/printing/print_browsertest.cc
@@ -2070,8 +2070,9 @@
content::RenderFrameHost* CreateFencedFrame(
content::RenderFrameHost* fenced_frame_parent,
const GURL& url) {
- if (fenced_frame_helper_)
+ if (fenced_frame_helper_) {
return fenced_frame_helper_->CreateFencedFrame(fenced_frame_parent, url);
+ }
// FencedFrameTestHelper only supports the MPArch version of fenced frames.
// So need to maually create a fenced frame for the ShadowDOM version.
@@ -2085,12 +2086,10 @@
content::JsReplace(kAddFencedFrameScript, url)));
EXPECT_TRUE(navigation.WaitForNavigationFinished());
- content::RenderFrameHost* new_frame = ChildFrameAt(fenced_frame_parent, 0);
-
- return new_frame;
+ return ChildFrameAt(fenced_frame_parent, 0);
}
- void RunPrintTest(const std::string& print_command) {
+ void RunScriptedPrintTest(const std::string& print_command) {
// Navigate to an initial page.
const GURL url(https_server_.GetURL("/empty.html"));
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
@@ -2122,9 +2121,9 @@
)";
const std::string test_script =
base::StringPrintf(kAddListenersScript, print_command.c_str());
-
EXPECT_EQ("beforeprint: false, afterprint: false",
content::EvalJs(fenced_frame_host, test_script));
+
ASSERT_TRUE(console_observer.Wait());
ASSERT_EQ(1u, console_observer.messages().size());
EXPECT_EQ(
@@ -2132,18 +2131,41 @@
console_observer.GetMessageAt(0));
}
+ void RunPrintTest() {
+ // Navigate to an initial page.
+ const GURL url(https_server_.GetURL("/empty.html"));
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+
+ // Load a fenced frame.
+ GURL fenced_frame_url = https_server_.GetURL("/fenced_frames/title1.html");
+ content::WebContents* web_contents =
+ browser()->tab_strip_model()->GetActiveWebContents();
+ content::RenderFrameHost* fenced_frame_host = CreateFencedFrame(
+ web_contents->GetPrimaryMainFrame(), fenced_frame_url);
+ ASSERT_TRUE(fenced_frame_host);
+
+ // `PrintViewManager` should refuse to print.
+ auto* print_view_manager = PrintViewManager::FromWebContents(web_contents);
+ ASSERT_TRUE(print_view_manager);
+ EXPECT_FALSE(print_view_manager->PrintPreviewNow(fenced_frame_host,
+ /*has_selection=*/false));
+ }
+
private:
- base::test::ScopedFeatureList feature_list_;
std::unique_ptr<content::test::FencedFrameTestHelper> fenced_frame_helper_;
net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
};
IN_PROC_BROWSER_TEST_F(PrintFencedFrameBrowserTest, ScriptedPrint) {
- RunPrintTest("window.print();");
+ RunScriptedPrintTest("window.print();");
}
IN_PROC_BROWSER_TEST_F(PrintFencedFrameBrowserTest, DocumentExecCommand) {
- RunPrintTest("document.execCommand('print');");
+ RunScriptedPrintTest("document.execCommand('print');");
+}
+
+IN_PROC_BROWSER_TEST_F(PrintFencedFrameBrowserTest, BrowserPrint) {
+ RunPrintTest();
}
#if BUILDFLAG(IS_WIN)
Original Bug Report
Fenced Frame isolation bypass and UI spoofing via PrintViewManager::RequestPrintPreview
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A missing fenced frame check in PrintViewManager::RequestPrintPreview allows a compromised fenced frame renderer to trigger a tab-modal print preview over the embedder. This leaks the embedder’s URL and title to the fenced frame and grants it a PrintPreviewUI remote, enabling UI spoofing.
Affected files:
chrome/browser/printing/print_view_manager.ccchrome/browser/ui/webui/print_preview/print_preview_handler.ccchrome/browser/printing/print_view_manager_base.cc
Estimated timestamp from git blame: 2024-04-24
Summary
A vulnerability in the printing::mojom::PrintManagerHost::RequestPrintPreview Mojo handler potentially allows a compromised fenced-frame renderer to bypass isolation guarantees. By sending a crafted request, an attacker can trigger a tab-modal print preview dialog over the embedder page. This leads to the disclosure of the embedder’s top-level URL and title to the fenced frame, as well as granting the fenced frame a PrintPreviewUI remote to control the dialog’s content.
Root Cause Analysis
In chrome/browser/printing/print_view_manager.cc, the RequestPrintPreview method and its callback OnRequestPrintPreviewCallback lack a check to ensure the requesting RenderFrameHost is not nested within a fenced frame.
While sibling methods such as SetupScriptedPrintPreview (used for window.print()) and PrintViewManagerBase::ScriptedPrint explicitly call rfh->IsNestedWithinFencedFrame() and reject requests from such frames with a bad_message, RequestPrintPreview does not perform this check.
Because fenced frames share the outer WebContents via the MPArch implementation, the PrintManagerHost interface (bound for all frames in ChromeContentBrowserClient::RegisterAssociatedInterfaceBindersForRenderFrameHost) allows a fenced-frame renderer to reach the PrintViewManager of the embedder’s WebContents. When a compromised fenced-frame renderer sends a RequestPrintPreview message with webnode_only = true, the browser proceeds to open a tab-modal chrome://print dialog over the embedder.
Information Disclosure and UI Spoofing
Once the print preview dialog is initialized, the chrome://print WebUI invokes PrintPreviewHandler::HandleGetPreview in chrome/browser/ui/webui/print_preview/print_preview_handler.cc.
This handler retrieves the initiator (the embedder’s WebContents) and fetches the active print preview frame via PrintViewManager::FromWebContents(initiator)->print_preview_rfh(). Because the attacker passed webnode_only = true in their request, print_preview_rfh_ points to the fenced frame’s RenderFrameHost.
The handler then reads the initiator’s title and URL to populate the preview settings if headers/footers are enabled (the default). These sensitive settings are sent back to the fenced frame via the PrintRenderFrame::PrintPreview(settings) Mojo call.
Furthermore, the handler binds a PrintPreviewUI remote to the fenced frame via print_render_frame_->SetPrintPreviewUI(...). This grants the attacker the ability to provide arbitrary PDF bytes (via MetafileReadyForPrinting) to be displayed in the preview pane, successfully spoofing a print UI over the embedder origin.
Impact
- Information Disclosure: The embedder page’s top-level URL and title are leaked to the fenced-frame renderer, violating the Privacy Sandbox guarantee that fenced frames cannot learn about their embedding environment.
- UI Spoofing: A compromised renderer can display attacker-supplied content within a trusted browser dialog (
chrome://print) shown as a tab-modal UI over the embedder origin.
Suggested Steps to Reproduce
Note: These are theoretical steps as our tooling agent cannot execute code.
- A victim visits a page that embeds an attacker-controlled
<fencedframe>. - The attacker achieves arbitrary code execution within the sandboxed renderer process of the fenced frame.
- From the compromised renderer, request the associated interface
printing::mojom::PrintManagerHoston the fenced frame’sLocalFrame. - Send a
RequestPrintPreviewmessage on this remote withwebnode_only = true. - Observe that a tab-modal
chrome://printdialog opens over the embedder tab. - The compromised renderer will receive the
PrintPreviewmessage containing the embedder’s URL and title, along with aPrintPreviewUIremote that can be used to control the dialog content.
Suggested Fix
Add an IsNestedWithinFencedFrame() check to PrintViewManager::RequestPrintPreview (similar to SetupScriptedPrintPreview) to terminate the renderer if a request is received from a fenced frame.
void PrintViewManager::RequestPrintPreview(
mojom::RequestPrintPreviewParamsPtr params) {
content::RenderFrameHost* rfh = GetCurrentTargetFrame();
if (rfh->IsNestedWithinFencedFrame()) {
bad_message::ReceivedBadMessage(
rfh->GetProcess(), bad_message::PVM_SCRIPTED_PRINT_FENCED_FRAME);
return;
}
// ... existing code ...
}
Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.