Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Printing
DescriptionInsufficient validation of untrusted input in Printing
ComponentPrinting
Bug ClassLogic Error
Tracker511776372
Fix commit07e3684d85aa (chromium/src) +54/-12
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
if
chrome/browser/printing/print_browsertest.cc
modified
IN_PROC_BROWSER_TEST_F
chrome/browser/printing/print_browsertest.cc
modified
if
chrome/browser/printing/print_view_manager.cc
modified

Files Changed

  • chrome/browser/bad_message.h
  • chrome/browser/printing/print_browsertest.cc
  • chrome/browser/printing/print_view_manager.cc
  • tools/metrics/histograms/metadata/stability/enums.xml
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">
Loading diff…

Regression Test / PoC

shipped with the fix
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)
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • chrome/browser/ui/webui/print_preview/print_preview_handler.cc
  • chrome/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

  1. 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.
  2. 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.

  1. A victim visits a page that embeds an attacker-controlled <fencedframe>.
  2. The attacker achieves arbitrary code execution within the sandboxed renderer process of the fenced frame.
  3. From the compromised renderer, request the associated interface printing::mojom::PrintManagerHost on the fenced frame’s LocalFrame.
  4. Send a RequestPrintPreview message on this remote with webnode_only = true.
  5. Observe that a tab-modal chrome://print dialog opens over the embedder tab.
  6. The compromised renderer will receive the PrintPreview message containing the embedder’s URL and title, along with a PrintPreviewUI remote 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.

View on issue tracker