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
Tracker513005991
Fix commitea38e74cf233 (chromium/src) +111/-90
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
chrome/browser/printing/print_view_manager_base.cc
modified

Files Changed

  • chrome/browser/printing/print_browsertest.cc
  • chrome/browser/printing/print_view_manager.cc
  • chrome/browser/printing/print_view_manager_base.cc
From ea38e74cf233e5ae1e5c3cc7d02d3c36e025a754 Mon Sep 17 00:00:00 2001
From: Lei Zhang <thestig@chromium.org>
Date: Fri, 22 May 2026 14:26:51 -0700
Subject: [PATCH] Print Preview: Remove PrintManagerHost.UpdatePrintSettings() dict param

In the Print Preview IPC dance, PrintPreviewHandler in the browser calls
PrintRenderFrame.PrintPreview() with a print settings dictionary.
PrintRenderFrameHelper, the PrintRenderFrame implementation,
conditionally makes a small edit to the dictionary, and passes it back
to PrintViewManagerBase in the browser in the
PrintManagerHost.UpdatePrintSettings() call.

In modern Chromium, the browser process can make the same conditional
decision and modify the dictionary itself. As such, there is no need for
PrintRenderFrameHelper to pass it back. So simplify the
UpdatePrintSettings() Mojo interface to remove the dictionary parameter.
After this, rename it to GetPrintPreviewParams() since it is no longer
sending an update to the browser.

On the browser side, pass the dictionary from PrintPreviewHandler to
PrintViewManagerBase, to compensate for the IPC change. The dictionary
needs to go into a queue as there may be multiple
PrintRenderFrame.PrintPreview() calls in quick succession.

Update tests as needed to match the IPC changes. In several test cases,
calls TestPrintViewManager::CreateForWebContents() to properly install
the TestPrintViewManager, instead of allocating it on the stack.

Bug: 513005991
Change-Id: Ie022900568c3bac7e086c9fbf9d6345a22e46278
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7866423
Reviewed-by: Andy Phan <andyphan@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
Reviewed-by: Tom Sepez <tsepez@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1635224}
---

diff --git a/chrome/browser/printing/print_browsertest.cc b/chrome/browser/printing/print_browsertest.cc
index f996b75..eaeb5a7 100644
--- a/chrome/browser/printing/print_browsertest.cc
+++ b/chrome/browser/printing/print_browsertest.cc
@@ -1585,13 +1585,13 @@
   content::WebContents* web_contents =
       browser()->tab_strip_model()->GetActiveWebContents();
   ASSERT_TRUE(web_contents);
-  TestPrintViewManager print_view_manager(web_contents);
-  PrintViewManager::SetReceiverImplForTesting(&print_view_manager);
+  TestPrintViewManager* print_view_manager =
+      TestPrintViewManager::CreateForWebContents(web_contents);
 
   PrintAndWaitUntilPreviewIsReady();
 
   const mojom::PrintPagesParamsPtr& snooped_params =
-      print_view_manager.snooped_params();
+      print_view_manager->snooped_params();
   ASSERT_TRUE(snooped_params);
   EXPECT_EQ(gfx::Size(kDefaultPdfDpi, kDefaultPdfDpi),
             snooped_params->params->dpi);
@@ -1632,13 +1632,13 @@
   content::WebContents* web_contents =
       browser()->tab_strip_model()->GetActiveWebContents();
   ASSERT_TRUE(web_contents);
-  TestPrintViewManager print_view_manager(web_contents);
-  PrintViewManager::SetReceiverImplForTesting(&print_view_manager);
+  TestPrintViewManager* print_view_manager =
+      TestPrintViewManager::CreateForWebContents(web_contents);
 
   PrintAndWaitUntilPreviewIsReady();
 
   const mojom::PrintPagesParamsPtr& snooped_params =
-      print_view_manager.snooped_params();
+      print_view_manager->snooped_params();
   ASSERT_TRUE(snooped_params);
   EXPECT_EQ(gfx::Size(kDefaultPdfDpi, kDefaultPdfDpi),
             snooped_params->params->dpi);
@@ -1667,15 +1667,12 @@
   content::WebContents* web_contents =
       browser()->tab_strip_model()->GetActiveWebContents();
   ASSERT_TRUE(web_contents);
-  TestPrintViewManager print_view_manager(web_contents);
-  PrintViewManager::SetReceiverImplForTesting(&print_view_manager);
+  TestPrintViewManager::CreateForWebContents(web_contents);
 
   // Override print parameters to do N-up, specify 4 pages per sheet.
   const PrintParams kParams{.pages_per_sheet = 4};
   PrintAndWaitUntilPreviewIsReady(kParams);
 
-  PrintViewManager::SetReceiverImplForTesting(nullptr);
-
   // With 4 pages per sheet requested by `GetPrintParams()`, a 7 page input
   // will result in 2 pages in the print preview.
   EXPECT_EQ(rendered_page_count(), 2u);
@@ -1690,15 +1687,12 @@
   content::WebContents* web_contents =
       browser()->tab_strip_model()->GetActiveWebContents();
   ASSERT_TRUE(web_contents);
-  TestPrintViewManager print_view_manager(web_contents);
-  PrintViewManager::SetReceiverImplForTesting(&print_view_manager);
+  TestPrintViewManager::CreateForWebContents(web_contents);
 
   // Override print parameters to do N-up, specify 4 pages per sheet.
   const PrintParams kParams{.pages_per_sheet = 4};
   PrintAndWaitUntilPreviewIsReady(kParams);
 
-  PrintViewManager::SetReceiverImplForTesting(nullptr);
-
   // With 4 pages per sheet requested by `GetPrintParams()`, a 7 page input
   // will result in 2 pages in the print preview.
   EXPECT_EQ(rendered_page_count(), 2u);
diff --git a/chrome/browser/printing/print_view_manager.cc b/chrome/browser/printing/print_view_manager.cc
index aa8dc760..03fe973 100644
--- a/chrome/browser/printing/print_view_manager.cc
+++ b/chrome/browser/printing/print_view_manager.cc
@@ -236,6 +236,7 @@
     MaybeUnblockScriptedPreviewRPH();
     scripted_print_preview_rph_ = nullptr;
   }
+  ClearPrintPreviewSettings();
   print_preview_state_ = NOT_PREVIEWING;
   print_preview_rfh_ = nullptr;
   for (auto& observer : GetTestObservers()) {
diff --git a/chrome/browser/printing/print_view_manager_base.cc b/chrome/browser/printing/print_view_manager_base.cc
index d90ef46f..7fb4bb03 100644
--- a/chrome/browser/printing/print_view_manager_base.cc
+++ b/chrome/browser/printing/print_view_manager_base.cc
@@ -255,6 +255,23 @@
 #endif
                      std::move(callback), std::move(printer_query)));
 }
+
+void PrintViewManagerBase::AppendPrintPreviewSettings(base::DictValue settings,
+                                                      bool is_pdf) {
+  CHECK(!settings.empty());
+  if (is_pdf) {
+    settings.Set(kSettingHeaderFooterEnabled, false);
+    settings.Set(kSettingMarginsType,
+                 static_cast<int>(mojom::MarginType::kNoMargins));
+  }
+  print_preview_settings_.push(std::move(settings));
+}
+
+void PrintViewManagerBase::ClearPrintPreviewSettings() {
+  while (!print_preview_settings_.empty()) {
+    print_preview_settings_.pop();
+  }
+}
 #endif  // BUILDFLAG(ENABLE_PRINT_PREVIEW)
 
 void PrintViewManagerBase::PrintToPdf(
@@ -307,7 +324,7 @@
     std::unique_ptr<PrinterQuery> printer_query,
     base::DictValue job_settings,
     std::unique_ptr<PrintSettings> print_settings,
-    UpdatePrintSettingsCallback callback,
+    GetPrintPreviewParamsCallback callback,
     bool success) {
   if (!success) {
     PRINTER_LOG(ERROR) << "Unable to update printable area for "
@@ -319,15 +336,15 @@
   }
   PRINTER_LOG(EVENT) << "Paper printable area updated for vendor id "
                      << print_settings->requested_media().vendor_id;
-  CompleteUpdatePrintSettings(std::move(job_settings),
-                              std::move(print_settings), std::move(callback));
+  CompleteGetPrintPreviewParams(std::move(job_settings),
+                                std::move(print_settings), std::move(callback));
 }
 #endif
 
-void PrintViewManagerBase::CompleteUpdatePrintSettings(
+void PrintViewManagerBase::CompleteGetPrintPreviewParams(
     base::DictValue job_settings,
     std::unique_ptr<PrintSettings> print_settings,
-    UpdatePrintSettingsCallback callback) {
+    GetPrintPreviewParamsCallback callback) {
   mojom::PrintPagesParamsPtr settings = mojom::PrintPagesParams::New();
   settings->pages = GetPageRangesFromJobSettings(job_settings);
   settings->params = mojom::PrintParams::New();
@@ -687,15 +704,23 @@
 }
 
 #if BUILDFLAG(ENABLE_PRINT_PREVIEW)
-void PrintViewManagerBase::UpdatePrintSettings(
-    base::DictValue job_settings,
-    UpdatePrintSettingsCallback callback) {
+void PrintViewManagerBase::GetPrintPreviewParams(
+    GetPrintPreviewParamsCallback callback) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
   if (!GetPrintingEnabledBooleanPref()) {
     std::move(callback).Run(nullptr);
     return;
   }
 
+  if (print_preview_settings_.empty()) {
+    std::move(callback).Run(nullptr);
+    return;
+  }
+
+  base::DictValue job_settings = std::move(print_preview_settings_.front());
+  print_preview_settings_.pop();
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 f996b75..eaeb5a7 100644
--- a/chrome/browser/printing/print_browsertest.cc
+++ b/chrome/browser/printing/print_browsertest.cc
@@ -1585,13 +1585,13 @@
   content::WebContents* web_contents =
       browser()->tab_strip_model()->GetActiveWebContents();
   ASSERT_TRUE(web_contents);
-  TestPrintViewManager print_view_manager(web_contents);
-  PrintViewManager::SetReceiverImplForTesting(&print_view_manager);
+  TestPrintViewManager* print_view_manager =
+      TestPrintViewManager::CreateForWebContents(web_contents);
 
   PrintAndWaitUntilPreviewIsReady();
 
   const mojom::PrintPagesParamsPtr& snooped_params =
-      print_view_manager.snooped_params();
+      print_view_manager->snooped_params();
   ASSERT_TRUE(snooped_params);
   EXPECT_EQ(gfx::Size(kDefaultPdfDpi, kDefaultPdfDpi),
             snooped_params->params->dpi);
@@ -1632,13 +1632,13 @@
   content::WebContents* web_contents =
       browser()->tab_strip_model()->GetActiveWebContents();
   ASSERT_TRUE(web_contents);
-  TestPrintViewManager print_view_manager(web_contents);
-  PrintViewManager::SetReceiverImplForTesting(&print_view_manager);
+  TestPrintViewManager* print_view_manager =
+      TestPrintViewManager::CreateForWebContents(web_contents);
 
   PrintAndWaitUntilPreviewIsReady();
 
   const mojom::PrintPagesParamsPtr& snooped_params =
-      print_view_manager.snooped_params();
+      print_view_manager->snooped_params();
   ASSERT_TRUE(snooped_params);
   EXPECT_EQ(gfx::Size(kDefaultPdfDpi, kDefaultPdfDpi),
             snooped_params->params->dpi);
@@ -1667,15 +1667,12 @@
   content::WebContents* web_contents =
       browser()->tab_strip_model()->GetActiveWebContents();
   ASSERT_TRUE(web_contents);
-  TestPrintViewManager print_view_manager(web_contents);
-  PrintViewManager::SetReceiverImplForTesting(&print_view_manager);
+  TestPrintViewManager::CreateForWebContents(web_contents);
 
   // Override print parameters to do N-up, specify 4 pages per sheet.
   const PrintParams kParams{.pages_per_sheet = 4};
   PrintAndWaitUntilPreviewIsReady(kParams);
 
-  PrintViewManager::SetReceiverImplForTesting(nullptr);
-
   // With 4 pages per sheet requested by `GetPrintParams()`, a 7 page input
   // will result in 2 pages in the print preview.
   EXPECT_EQ(rendered_page_count(), 2u);
@@ -1690,15 +1687,12 @@
   content::WebContents* web_contents =
       browser()->tab_strip_model()->GetActiveWebContents();
   ASSERT_TRUE(web_contents);
-  TestPrintViewManager print_view_manager(web_contents);
-  PrintViewManager::SetReceiverImplForTesting(&print_view_manager);
+  TestPrintViewManager::CreateForWebContents(web_contents);
 
   // Override print parameters to do N-up, specify 4 pages per sheet.
   const PrintParams kParams{.pages_per_sheet = 4};
   PrintAndWaitUntilPreviewIsReady(kParams);
 
-  PrintViewManager::SetReceiverImplForTesting(nullptr);
-
   // With 4 pages per sheet requested by `GetPrintParams()`, a 7 page input
   // will result in 2 pages in the print preview.
   EXPECT_EQ(rendered_page_count(), 2u);
diff --git a/chrome/browser/printing/system_access_process_print_browsertest.cc b/chrome/browser/printing/system_access_process_print_browsertest.cc
index e7fe9e5..898139e 100644
--- a/chrome/browser/printing/system_access_process_print_browsertest.cc
+++ b/chrome/browser/printing/system_access_process_print_browsertest.cc
@@ -1512,15 +1512,15 @@
   content::WebContents* web_contents =
       browser()->tab_strip_model()->GetActiveWebContents();
   ASSERT_TRUE(web_contents);
-  TestPrintViewManager print_view_manager(web_contents);
-  PrintViewManager::SetReceiverImplForTesting(&print_view_manager);
+  TestPrintViewManager* print_view_manager =
+      TestPrintViewManager::CreateForWebContents(web_contents);
 
   PrintAndWaitUntilPreviewIsReady();
 
   EXPECT_EQ(3u, rendered_page_count());
 
   const mojom::PrintPagesParamsPtr& snooped_params =
-      print_view_manager.snooped_params();
+      print_view_manager->snooped_params();
   ASSERT_TRUE(snooped_params);
   EXPECT_EQ(test::kPrinterCapabilitiesDpi, snooped_params->params->dpi);
 
@@ -1546,15 +1546,15 @@
   content::WebContents* web_contents =
       browser()->tab_strip_model()->GetActiveWebContents();
   ASSERT_TRUE(web_contents);
-  TestPrintViewManager print_view_manager(web_contents);
-  PrintViewManager::SetReceiverImplForTesting(&print_view_manager);
+  TestPrintViewManager* print_view_manager =
+      TestPrintViewManager::CreateForWebContents(web_contents);
 
   AdjustMediaAfterPreviewIsReadyAndLoaded();
 
   EXPECT_EQ(1u, rendered_page_count());
 
   const mojom::PrintPagesParamsPtr& snooped_params =
-      print_view_manager.snooped_params();
+      print_view_manager->snooped_params();
   ASSERT_TRUE(snooped_params);
   EXPECT_EQ(test::kPrinterCapabilitiesDpi, snooped_params->params->dpi);
diff --git a/components/printing/test/print_render_frame_helper_browsertest.cc b/components/printing/test/print_render_frame_helper_browsertest.cc
index ff247c4..506ac8f0 100644
--- a/components/printing/test/print_render_frame_helper_browsertest.cc
+++ b/components/printing/test/print_render_frame_helper_browsertest.cc
@@ -358,9 +358,9 @@
     std::move(callback).Run(std::move(settings));
   }
 #if BUILDFLAG(ENABLE_PRINT_PREVIEW)
-  void UpdatePrintSettings(base::DictValue job_settings,
-                           UpdatePrintSettingsCallback callback) override {
+  void GetPrintPreviewParams(GetPrintPreviewParamsCallback callback) override {
     // Check and make sure the required settings are all there.
+    const base::DictValue& job_settings = job_settings_;
     std::optional<int> margins_type = job_settings.FindInt(kSettingMarginsType);
     if (!margins_type.has_value() ||
         !job_settings.FindBool(kSettingLandscape) ||
@@ -449,7 +449,11 @@
   void set_preview_ui(FakePrintPreviewUI* preview_ui) {
     preview_ui_ = preview_ui;
   }
-#endif
+
+  void set_job_settings(const base::DictValue& settings) {
+    job_settings_ = settings.Clone();
+  }
+#endif  // BUILDFLAG(ENABLE_PRINT_PREVIEW)
 
   int accessibility_tree_set_count() const {
     return accessibility_tree_set_count_;
@@ -478,6 +482,7 @@
   raw_ptr<MockPrinter> printer_;
 #if BUILDFLAG(ENABLE_PRINT_PREVIEW)
   raw_ptr<FakePrintPreviewUI> preview_ui_;
+  base::DictValue job_settings_;
 #endif
   base::OnceClosure quit_closure_;
   bool is_printing_enabled_ = true;
@@ -1735,6 +1740,7 @@
   }
 
   void OnPrintPreview() {
+    print_manager()->set_job_settings(print_settings());
     PrintRenderFrameHelper* print_render_frame_helper =
         GetPrintRenderFrameHelper();
     print_render_frame_helper->InitiatePrintPreview(
@@ -1742,7 +1748,7 @@
         mojo::NullAssociatedRemote(),
 #endif
         /*has_selection=*/false);
-    print_render_frame_helper->PrintPreview(print_settings_.Clone());
+    print_render_frame_helper->PrintPreview(print_settings().Clone());
     preview_ui()->WaitUntilPreviewUpdate();
 
 #if defined(MOCK_PRINTER_SUPPORTS_PAGE_IMAGES)
@@ -1756,8 +1762,9 @@
   }
 
   void OnPrintPreviewRerender() {
+    print_manager()->set_job_settings(print_settings());
     preview_ui()->ResetPreviewStatus();
-    GetPrintRenderFrameHelper()->PrintPreview(print_settings_.Clone());
+    GetPrintRenderFrameHelper()->PrintPreview(print_settings().Clone());
     preview_ui()->WaitUntilPreviewUpdate();
   }
 
@@ -1769,6 +1776,7 @@
     content::RenderFrame* render_frame =
         content::RenderFrame::FromWebFrame(frame);
     BindPrintManagerHost(render_frame);
+    print_manager(render_frame)->set_job_settings(print_settings());
     PrintRenderFrameHelper* print_render_frame_helper =
         GetPrintRenderFrameHelperForFrame(render_frame);
     print_render_frame_helper->SetPrintPreviewUI(preview_ui->BindReceiver());
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential authenticated NTLM leak in browser process via unvalidated printer names

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 compromised renderer can provide a malicious UNC path as a printer name, which is processed unvalidated by the browser process. On Windows, this leads to a privileged API call that can trigger an authenticated NTLM connection to an attacker-controlled host, leaking credentials.

Affected files:

  • chrome/browser/printing/print_backend_service_manager.cc
  • chrome/browser/printing/printer_query_oop.cc
  • chrome/browser/printing/print_view_manager_base.cc

Estimated timestamp from git blame: 2021-07-09

Summary

A potential vulnerability exists in the Chrome printing stack on Windows where an unvalidated printer name (device name) supplied by a renderer process is used in a privileged GDI/Spooler API call within the browser process. A compromised renderer can leverage this to force the browser process to initiate an authenticated NTLM connection to an arbitrary UNC path, leading to the leakage of the user’s NTLM credentials and enabling authenticated SSRF attacks.

Root Cause

In chrome/browser/printing/print_view_manager_base.cc, the UpdatePrintSettings Mojo method receives print settings from the renderer as a dictionary. This dictionary contains a deviceName field which is intended to be the name of the target printer.

The function PrintSettingsFromJobSettings in printing/print_settings_conversion.cc extracts this name and stores it in a PrintSettings object without performing any validation or sanitization on the string content.

On Windows, when the printer type is kLocal, the browser process proceeds to query printer information. This eventually leads to a call to PrintBackend::GetPrinterDriverInfo(printer_name). In the Windows implementation (printing/backend/print_backend_win.cc), this function calls ::OpenPrinter with the attacker-controlled string.

Even when Out-of-Process (OOP) printing is enabled, the browser process still performs an in-process call to GetPrinterDriverInfo within PrintBackendServiceManager::SetCrashKeys for crash reporting purposes. This bypasses the isolation that OOP printing aims to provide for printer driver interactions.

Potential Attack Path

  1. Renderer Compromise: An attacker gains control over a renderer process.
  2. Malicious IPC: The compromised renderer sends an UpdatePrintSettings IPC to the browser process with a malicious deviceName set to a UNC path (e.g., \\attacker.evil\fake_printer).
  3. Browser Execution: PrintViewManagerBase in the browser process receives the request and, for local printers, invokes PrinterQuery::UpdatePrintableArea or PrintBackendServiceManager::GetPaperPrintableArea.
  4. Privileged Sink: These functions call GetPrinterDriverInfo in the browser process, which executes ::OpenPrinter on the malicious UNC path.
  5. Credential Leak: The Windows Spooler service attempts to connect to the UNC path. The attacker’s server at attacker.evil requests authentication, and the browser process sends the user’s NTLM hash.

Impact

  • NTLM Credential Leak: Capture of the user’s NTLM hash for offline cracking or NTLM relay attacks.
  • Authenticated SSRF: Forcing the browser process to make SMB/RPC requests to internal or external network resources.
  • Security Boundary Bypass: Bypassing the isolation of the printing stack by forcing privileged calls in the browser process using unvalidated renderer data.

Suggested Fix

The browser process must validate that the deviceName provided by the renderer corresponds to an actually installed and available printer on the system before passing it to any backend or crash-reporting functions. Validation should occur at the IPC entry point in PrintViewManagerBase or during the conversion in PrintSettingsFromJobSettings.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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.

View on issue tracker