CVE-2026-10971
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/printing/print_view_manager_base.cc |
modified |
Files Changed
chrome/browser/printing/print_browsertest.ccchrome/browser/printing/print_view_manager.ccchrome/browser/printing/print_view_manager_base.cc
Patch
From ea38e74cf233e5ae1e5c3cc7d02d3c36e025a754 Mon Sep 17 00:00:00 2001 From: Lei Zhang <[email protected]> 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 <[email protected]> Commit-Queue: Lei Zhang <[email protected]> Reviewed-by: Tom Sepez <[email protected]> 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();
Regression Test / PoC
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());
Original Bug Report
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.ccchrome/browser/printing/printer_query_oop.ccchrome/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
- Renderer Compromise: An attacker gains control over a renderer process.
- Malicious IPC: The compromised renderer sends an
UpdatePrintSettingsIPC to the browser process with a maliciousdeviceNameset to a UNC path (e.g.,\\attacker.evil\fake_printer). - Browser Execution:
PrintViewManagerBasein the browser process receives the request and, for local printers, invokesPrinterQuery::UpdatePrintableAreaorPrintBackendServiceManager::GetPaperPrintableArea. - Privileged Sink: These functions call
GetPrinterDriverInfoin the browser process, which executes::OpenPrinteron the malicious UNC path. - Credential Leak: The Windows Spooler service attempts to connect to the UNC path. The attacker’s server at
attacker.evilrequests 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.