Medium watch CVSS 8.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Printing
DescriptionImproper input validation in Printing
ComponentPrinting
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker498367544
Fix commit3d63d12873dc (chromium/src) +82/-36
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-01 (per vendor advisory)
Fix landed2026-07-23 (public git)
Disclosed2026-08-25
Patch gap33 days — fix public in source before the advisory

Background

Print Preview
Chrome’s browser-process UI that renders a preview of a document before printing; its behavior differs for HTML documents (modifiable) versus PDF documents.
`mojom::RequestPrintPreviewParams::is_modifiable`
a bit passed over IPC in the print-preview request params that indicates whether the content is modifiable; it can be set by a renderer process and is therefore not trustworthy.
`PrintPreviewDialogController` / `InitiatorData`
the browser-process controller that owns per-dialog state (InitiatorData), tracking the initiator WebContents and its request params.
`RenderProcessHost::IsPdf()`
a browser-side check indicating whether the initiator’s render process is a PDF process, a trustworthy source of the PDF-vs-HTML distinction.

Root Cause Analysis

Whether Print Preview treats content as a PDF was derived from request_params->is_modifiable (via is_pdf = !request_params->is_modifiable), a bit carried in mojom::RequestPrintPreviewParams that a renderer process can set, making it untrustworthy. Consumers such as PrintPreviewHandler (metrics and initial settings), PrintPreviewUI, and others read this renderer-influenced value to decide the modifiable/PDF nature of the preview.

The fix stops trusting that IPC bit for this decision: it introduces a separate is_pdf value computed in the browser process from render_frame_host->GetProcess()->IsPdf() and stored in InitiatorData::is_pdf, threaded through PrintPreview → GetOrCreatePreviewDialog → CreatePrintPreviewDialog. Callers now read it via PrintPreviewDialogController::IsPrintingPdf() / PrintPreviewHandler::IsPrintingPdf() (returning std::optional<bool>) rather than via GetRequestParams()->is_modifiable, and initial_settings is set from !is_pdf. The invariant restored is that the PDF/modifiable determination comes only from a browser-trusted source, not from renderer-supplied IPC data.

Key insight
The core mistake was deriving a security-relevant PDF/modifiable determination from the renderer-settable is_modifiable IPC bit; the fix stores a browser-computed is_pdf (from RenderProcessHost::IsPdf()) in the dialog controller and exposes it via IsPrintingPdf() so consumers no longer trust renderer input.

Attack Path

  1. Initiate a print preview A renderer triggers Print Preview, sending mojom::RequestPrintPreviewParams that includes the is_modifiable bit.
  2. Spoof the modifiable bit A compromised or malicious renderer sets is_modifiable to a value inconsistent with the actual content type (e.g. claiming HTML/modifiable for PDF content, or vice versa).
  3. Browser trusts the spoofed bit Pre-patch, PrintPreviewHandler and PrintPreviewUI computed is_pdf/modifiable from request_params->is_modifiable, so the preview’s document-type handling, initial settings (kSettingPreviewModifiable), and metrics followed the renderer-controlled value.
  4. Misclassified preview behavior The preview is presented/handled as the wrong document type based on renderer input (inferred consequence), which the fix prevents by sourcing is_pdf from the browser.

Impact Assessment

A renderer could influence Print Preview’s PDF-vs-modifiable determination by supplying an untrustworthy is_modifiable bit over IPC, an input-validation/trust-boundary weakness in the browser-process print path; the patch is a hardening/migration that moves the decision to a browser-computed is_pdf. Preconditions are the ability to trigger Print Preview and control the request params from a renderer. The advisory classifies this as improper input validation in Printing; no exploited-in-the-wild status is stated, and any concrete downstream security consequence beyond misclassification is not demonstrated by the patch and is (inferred).

Changed Functions

FunctionChangeNotes
if
chrome/browser/printing/print_preview_dialog_controller.cc
modified
if
chrome/browser/ui/webui/print_preview/print_preview_handler.cc
modified
GetRequestParams
chrome/browser/ui/webui/print_preview/print_preview_handler.cc
modified

Files Changed

  • chrome/browser/ui/webui/print_preview/print_preview_handler.h
  • chrome/browser/printing/print_preview_dialog_controller.cc
  • chrome/browser/ui/webui/print_preview/print_preview_handler.cc
  • chrome/browser/printing/print_view_manager.cc
  • chrome/browser/printing/print_preview_dialog_controller.h
  • chrome/browser/ui/webui/print_preview/print_preview_ui.cc

Audit Directions

  • Renderer-settable IPC bits used for browser decisions
    Search for browser-process logic that keys behavior on booleans/fields inside renderer-supplied mojom structs (like is_modifiable); prefer a browser-computed source of truth.
  • Process-type determinations
    Where PDF/plugin/guest distinctions matter, confirm they come from RenderProcessHost::IsPdf()/browser state rather than from data the renderer can set.
  • Duplicated trust of the same field
    When one untrusted bit is read in multiple consumers (PrintPreviewHandler, PrintPreviewUI, metrics), audit all readers together, since fixing one site may leave others trusting the stale value.
From 3d63d12873dcf84fa15de78a3b4c52c1aa113b27 Mon Sep 17 00:00:00 2001
From: Lei Zhang <[email protected]>
Date: Wed, 22 Jul 2026 18:01:15 -0700
Subject: [PATCH] Print Preview: Store is_pdf bit outside of RequestPrintPreviewParams

The mojom::RequestPrintPreviewParams struct has an `is_modifiable` bit
that is potentially untrustworthy if it is set by a renderer process.
It is also confusing because sometimes the browser process sets this
bit. In the long run, there is no need for `is_modifiable` to exist, as
the browser process can figure out the value on its own. So it does not
need to be passed over IPC via RequestPrintPreviewParams.

Start migrating away from it by storing a separate `is_pdf` bit in
PrintPreviewDialogController. This bit comes from the browser process
and never from a renderer process. Allow callers to read it via
IsPrintingPdf() instead of reading `is_modifiable` via
GetRequestParams(). Convert GetRequestParams() callers to use
IsPrintingPdf() where possible.

Bug: 498367544
Change-Id: I529977a18dd52c7c0ca3fc69fa75bec0364e744c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8129157
Commit-Queue: Lei Zhang <[email protected]>
Reviewed-by: Andy Phan <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1666757}
---
diff --git a/chrome/browser/printing/print_preview_dialog_controller.cc b/chrome/browser/printing/print_preview_dialog_controller.cc
index e316d44..3bf649c 100644
--- a/chrome/browser/printing/print_preview_dialog_controller.cc
+++ b/chrome/browser/printing/print_preview_dialog_controller.cc
@@ -195,12 +195,13 @@
 
 void PrintPreviewDialogController::PrintPreview(
     WebContents* initiator,
-    const mojom::RequestPrintPreviewParams& params) {
+    const mojom::RequestPrintPreviewParams& params,
+    bool is_pdf) {
   if (initiator->IsCrashed()) {
     return;
   }
 
-  if (!GetOrCreatePreviewDialog(initiator, params)) {
+  if (!GetOrCreatePreviewDialog(initiator, params, is_pdf)) {
     auto* print_view_manager = PrintViewManager::FromWebContents(initiator);
     if (print_view_manager) {
       print_view_manager->PrintPreviewDone();
@@ -217,14 +218,16 @@
 
 WebContents* PrintPreviewDialogController::GetOrCreatePreviewDialogForTesting(
     WebContents* initiator) {
+  constexpr bool kIsPdf = false;
   mojom::RequestPrintPreviewParams params;
-  params.is_modifiable = true;
-  return GetOrCreatePreviewDialog(initiator, params);
+  params.is_modifiable = !kIsPdf;
+  return GetOrCreatePreviewDialog(initiator, params, kIsPdf);
 }
 
 WebContents* PrintPreviewDialogController::GetOrCreatePreviewDialog(
     WebContents* initiator,
-    const mojom::RequestPrintPreviewParams& params) {
+    const mojom::RequestPrintPreviewParams& params,
+    bool is_pdf) {
   DCHECK(initiator);
 
   // Get the print preview dialog for `initiator`.
@@ -242,7 +245,7 @@
     return nullptr;
   }
 
-  return CreatePrintPreviewDialog(tab, initiator, params);
+  return CreatePrintPreviewDialog(tab, initiator, params, is_pdf);
 }
 
 WebContents* PrintPreviewDialogController::GetPrintPreviewForContents(
@@ -276,6 +279,15 @@
   return it != preview_dialog_map_.end() ? &it->second.request_params : nullptr;
 }
 
+std::optional<bool> PrintPreviewDialogController::IsPrintingPdf(
+    content::WebContents* preview_dialog) const {
+  auto it = preview_dialog_map_.find(preview_dialog);
+  if (it != preview_dialog_map_.end()) {
+    return it->second.is_pdf;
+  }
+  return std::nullopt;
+}
+
 void PrintPreviewDialogController::ForEachPreviewDialog(
     base::RepeatingCallback<void(content::WebContents*)> callback) {
   for (const auto& it : preview_dialog_map_)
@@ -303,6 +315,8 @@
   web_contents_collection_.StopObserving(it->second.initiator);
   it->second.initiator = nullptr;
   it->second.request_params = {};
+  // Set to true to match the behavior for resetting `request_params`.
+  it->second.is_pdf = true;
   it->second.scoper.reset();
 }
 
@@ -318,9 +332,11 @@
 PrintPreviewDialogController::InitiatorData::InitiatorData(
     content::WebContents* initiator,
     const mojom::RequestPrintPreviewParams& request_params,
+    bool is_pdf,
     std::unique_ptr<tabs::ScopedTabModalUI> scoper)
     : initiator(initiator),
       request_params(request_params),
+      is_pdf(is_pdf),
       scoper(std::move(scoper)) {}
 
 PrintPreviewDialogController::InitiatorData::~InitiatorData() = default;
@@ -424,7 +440,8 @@
 WebContents* PrintPreviewDialogController::CreatePrintPreviewDialog(
     tabs::TabInterface* tab,
     content::WebContents* initiator,
-    const mojom::RequestPrintPreviewParams& params) {
+    const mojom::RequestPrintPreviewParams& params,
+    bool is_pdf) {
   base::AutoReset<bool> auto_reset(&is_creating_print_preview_dialog_, true);
 
   // The dialog delegates are deleted when the dialog is closed.
@@ -444,7 +461,8 @@
   PrintViewManager::CreateForWebContents(preview_dialog);
 
   // Add an entry to the map.
-  InitiatorData data(initiator, params, tab ? tab->ShowModalUI() : nullptr);
+  InitiatorData data(initiator, params, is_pdf,
+                     tab ? tab->ShowModalUI() : nullptr);
   preview_dialog_map_.emplace(preview_dialog, std::move(data));
 
   // Make the print preview WebContents show up in the task manager.
diff --git a/chrome/browser/printing/print_preview_dialog_controller.h b/chrome/browser/printing/print_preview_dialog_controller.h
index 77fea0a8..23b5b35 100644
--- a/chrome/browser/printing/print_preview_dialog_controller.h
+++ b/chrome/browser/printing/print_preview_dialog_controller.h
@@ -7,6 +7,7 @@
 
 #include <map>
 #include <memory>
+#include <optional>
 
 #include "base/check.h"
 #include "base/functional/callback.h"
@@ -57,7 +58,8 @@
 
   // Initiates print preview for `initiator`.
   void PrintPreview(content::WebContents* initiator,
-                    const mojom::RequestPrintPreviewParams& params);
+                    const mojom::RequestPrintPreviewParams& params,
+                    bool is_pdf);
 
   // Returns the preview dialog for `contents`.
   // Returns `contents` if `contents` is a preview dialog.
@@ -74,6 +76,10 @@
   const mojom::RequestPrintPreviewParams* GetRequestParams(
       content::WebContents* preview_dialog) const;
 
+  // Returns whether the initiator associated with `preview_dialog` is printing
+  // PDF content or not. Returns nullopt if no data exists for `preview_dialog`.
+  std::optional<bool> IsPrintingPdf(content::WebContents* preview_dialog) const;
+
   // Runs `callback` on the dialog of each active print preview operation.
   void ForEachPreviewDialog(
       base::RepeatingCallback<void(content::WebContents*)> callback);
@@ -95,9 +101,11 @@
                                         content::WebContents* preview_dialog) {
     CHECK(initiator);
     CHECK(preview_dialog);
+
+    constexpr bool kIsPdf = false;
     mojom::RequestPrintPreviewParams params;
-    params.is_modifiable = true;
-    InitiatorData data(initiator, params, /*scoper=*/nullptr);
+    params.is_modifiable = !kIsPdf;
+    InitiatorData data(initiator, params, kIsPdf, /*scoper=*/nullptr);
     preview_dialog_map_.emplace(preview_dialog, std::move(data));
   }
   void DisassociateWebContentsesForTesting(
@@ -117,6 +125,7 @@
   struct InitiatorData {
     InitiatorData(content::WebContents* initiator,
                   const mojom::RequestPrintPreviewParams& request_params,
+                  bool is_pdf,
                   std::unique_ptr<tabs::ScopedTabModalUI> scoper);
     InitiatorData(InitiatorData&&) noexcept;
     InitiatorData& operator=(InitiatorData&&) noexcept;
@@ -124,6 +133,7 @@
 
     raw_ptr<content::WebContents> initiator;
     mojom::RequestPrintPreviewParams request_params;
+    bool is_pdf;
 
     // Prevents other tab-modal UIs from showing.
     std::unique_ptr<tabs::ScopedTabModalUI> scoper;
@@ -159,14 +169,16 @@
   // Gets/Creates the print preview dialog for `initiator`.
   content::WebContents* GetOrCreatePreviewDialog(
       content::WebContents* initiator,
-      const mojom::RequestPrintPreviewParams& params);
+      const mojom::RequestPrintPreviewParams& params,
+      bool is_pdf);
 
   // Creates a new print preview dialog if GetOrCreatePreviewDialog() cannot
   // find a print preview dialog for `initiator`.
   content::WebContents* CreatePrintPreviewDialog(
       tabs::TabInterface* tab,
       content::WebContents* initiator,
-      const mojom::RequestPrintPreviewParams& params);
+      const mojom::RequestPrintPreviewParams& params,
+      bool is_pdf);
 
   // Helper function to store the title of the initiator associated with
   // `preview_dialog` in `preview_dialog`'s PrintPreviewUI.
diff --git a/chrome/browser/printing/print_view_manager.cc b/chrome/browser/printing/print_view_manager.cc
index d22df27..db182ee 100644
--- a/chrome/browser/printing/print_view_manager.cc
+++ b/chrome/browser/printing/print_view_manager.cc
@@ -581,8 +581,9 @@
   auto* dialog_controller = PrintPreviewDialogController::GetInstance();
   CHECK(dialog_controller);
   mojom::RequestPrintPreviewParams params;
-  params.is_modifiable = !print_preview_rfh_->GetProcess()->IsPdf();
-  dialog_controller->PrintPreview(web_contents(), params);
+  const bool is_pdf = print_preview_rfh_->GetProcess()->IsPdf();
+  params.is_modifiable = !is_pdf;
+  dialog_controller->PrintPreview(web_contents(), params, is_pdf);
 
   PrintPreviewAllowedForTesting();
 }
@@ -632,7 +633,8 @@
 
   auto* dialog_controller = PrintPreviewDialogController::GetInstance();
   CHECK(dialog_controller);
-  dialog_controller->PrintPreview(web_contents(), *params);
+  dialog_controller->PrintPreview(web_contents(), *params,
+                                  render_frame_host->GetProcess()->IsPdf());
 
   PrintPreviewAllowedForTesting();
 }
diff --git a/chrome/browser/ui/webui/print_preview/print_preview_handler.cc b/chrome/browser/ui/webui/print_preview/print_preview_handler.cc
index 93e85920..9ed3579c 100644
--- a/chrome/browser/ui/webui/print_preview/print_preview_handler.cc
+++ b/chrome/browser/ui/webui/print_preview/print_preview_handler.cc
@@ -673,17 +673,17 @@
   }
   DCHECK(data->size());
 
-  // After validating |settings|, record metrics.
-  const mojom::RequestPrintPreviewParams* request_params = GetRequestParams();
-  CHECK(request_params);
-  bool is_pdf = !request_params->is_modifiable;
+  // After validating `settings`, record metrics.
+  std::optional<bool> maybe_is_pdf = IsPrintingPdf();
+  CHECK(maybe_is_pdf.has_value());
   if (last_preview_settings_.has_value()) {
-    ReportPrintSettingsStats(settings, last_preview_settings_.value(), is_pdf);
+    ReportPrintSettingsStats(settings, last_preview_settings_.value(),
+                             maybe_is_pdf.value());
   }
   {
     PrintDocumentTypeBuckets doc_type =
-        is_pdf ? PrintDocumentTypeBuckets::kPdfDocument
-               : PrintDocumentTypeBuckets::kHtmlDocument;
+        maybe_is_pdf.value() ? PrintDocumentTypeBuckets::kPdfDocument
+                             : PrintDocumentTypeBuckets::kHtmlDocument;
     ReportPrintDocumentTypeHistograms(doc_type);
   }
   ReportUserActionHistogram(user_action);
@@ -861,20 +861,33 @@
     const std::string& callback_id,
     base::DictValue policies,
     const std::string& default_printer) {
-  const mojom::RequestPrintPreviewParams* request_params = GetRequestParams();
+  bool is_pdf;
   mojom::RequestPrintPreviewParams dummy_params;
-  if (!request_params) {
+
+  // `request_params` and `maybe_is_pdf` should both be non-null or both be
+  // null.
+  auto* dialog_controller = PrintPreviewDialogController::GetInstance();
+  CHECK(dialog_controller);
+  const mojom::RequestPrintPreviewParams* request_params =
+      dialog_controller->GetRequestParams(preview_web_contents());
+  std::optional<bool> maybe_is_pdf = IsPrintingPdf();
+  if (request_params) {
+    CHECK(maybe_is_pdf.has_value());
+    is_pdf = maybe_is_pdf.value();
+  } else {
     // This only happens with a direct navigation to chrome://print, which can
     // happen in some tests. Just use `dummy_params` to set up the test with
     // some sane values, so it does not crash.
-    dummy_params.is_modifiable = true;
+    CHECK(!maybe_is_pdf.has_value());
+    constexpr bool kIsPdf = false;
+    is_pdf = kIsPdf;
+    dummy_params.is_modifiable = !kIsPdf;
     request_params = &dummy_params;
   }
 
   base::DictValue initial_settings;
   initial_settings.Set(kDocumentTitle, print_preview_ui()->initiator_title());
-  initial_settings.Set(kSettingPreviewModifiable,
-                       request_params->is_modifiable);
+  initial_settings.Set(kSettingPreviewModifiable, !is_pdf);
 #if BUILDFLAG(IS_CHROMEOS)
   initial_settings.Set(kSettingPreviewIsFromArc, request_params->is_from_arc);
 #endif
@@ -953,11 +966,10 @@
   return dialog_controller->GetInitiator(preview_web_contents());
 }
 
-const mojom::RequestPrintPreviewParams*
-PrintPreviewHandler::GetRequestParams() {
+std::optional<bool> PrintPreviewHandler::IsPrintingPdf() {
   auto* dialog_controller = PrintPreviewDialogController::GetInstance();
   CHECK(dialog_controller);
-  return dialog_controller->GetRequestParams(preview_web_contents());
+  return dialog_controller->IsPrintingPdf(preview_web_contents());
 }
 
 void PrintPreviewHandler::OnPrintPreviewReady(
diff --git a/chrome/browser/ui/webui/print_preview/print_preview_handler.h b/chrome/browser/ui/webui/print_preview/print_preview_handler.h
index 7432256..b65fcda 100644
--- a/chrome/browser/ui/webui/print_preview/print_preview_handler.h
+++ b/chrome/browser/ui/webui/print_preview/print_preview_handler.h
@@ -147,7 +147,9 @@
 
   PrintPreviewUI* print_preview_ui();
 
-  const mojom::RequestPrintPreviewParams* GetRequestParams();
+  // Returns whether `this` is printing a PDF or not. Returns std::nullopt if
+  // the associated Print Preview dialog does not exist.
+  std::optional<bool> IsPrintingPdf();
 
   PrefService* GetPrefs();
 
diff --git a/chrome/browser/ui/webui/print_preview/print_preview_ui.cc b/chrome/browser/ui/webui/print_preview/print_preview_ui.cc
index d5b35950..c4160fae 100644
--- a/chrome/browser/ui/webui/print_preview/print_preview_ui.cc
+++ b/chrome/browser/ui/webui/print_preview/print_preview_ui.cc
@@ -584,10 +584,10 @@
 
   auto* dialog_controller = PrintPreviewDialogController::GetInstance();
   CHECK(dialog_controller);
-  const mojom::RequestPrintPreviewParams* request_params =
-      dialog_controller->GetRequestParams(web_ui()->GetWebContents());
-  CHECK(request_params);
-  return request_params->is_modifiable;
+  std::optional<bool> maybe_is_pdf =
+      dialog_controller->IsPrintingPdf(web_ui()->GetWebContents());
+  CHECK(maybe_is_pdf.has_value());
+  return !maybe_is_pdf.value();
 }
 
 void PrintPreviewUI::OnCompositePdfPageDone(
Loading diff…

Original Bug Report

reported by [email protected]

Potential Sandbox Escape via Forged is_modifiable Flag in Print IPC on macOS

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A compromised renderer can forge the is_modifiable flag in print preview IPCs to bypass the sandboxed PrintCompositor. This allows the attacker to send arbitrary malicious PDF bytes to the unsandboxed PrintBackendService on macOS. Parsing these bytes with Apple CoreGraphics provides a potential path for a full sandbox escape.

Affected files:

  • chrome/browser/printing/print_view_manager.cc
  • chrome/browser/ui/webui/print_preview/print_preview_ui.cc
  • printing/pdf_metafile_cg_mac.cc
  • chrome/browser/printing/print_view_manager_base.cc
  • chrome/browser/ui/webui/print_preview/print_preview_handler.cc
  • chrome/browser/printing/print_preview_data_service.cc
  • chrome/services/printing/print_backend_service_impl.cc

Estimated timestamp from git blame: 2026-02-10

Summary

There is a potential sandbox escape vulnerability in the Chrome printing architecture on macOS. A compromised HTML renderer can forge the is_modifiable flag during a print preview request. Because the browser fails to validate this flag against the actual renderer type, the attacker can bypass the sandboxed PrintCompositor and directly supply malicious PDF bytes. When printed, these bytes are parsed by Apple’s CoreGraphics library within an unsandboxed utility process (or the browser process), creating a direct path to a sandbox escape.

Technical Details

1. Unvalidated IPC Parameter When a renderer initiates a print preview, it sends a RequestPrintPreview or ShowScriptedPrintPreview IPC to PrintViewManager (chrome/browser/printing/print_view_manager.cc). The renderer provides a mojom::RequestPrintPreviewParams struct containing an is_modifiable boolean. The browser passes this parameter directly to the PrintPreviewDialogController without verifying if the renderer actually hosts a PDF document (e.g., by checking render_frame_host->GetProcess()->IsPdf()).

2. PrintCompositor Bypass When the WebUI triggers PrintPreviewUI::MetafileReadyForPrinting (chrome/browser/ui/webui/print_preview/print_preview_ui.cc), the browser calls ShouldUseCompositor(). This function simply returns the unvalidated is_modifiable flag. If the attacker sets this to false, the browser bypasses the sandboxed PrintCompositor and accepts the renderer’s raw shared memory region as a valid PDF document. The only validation performed in release builds is LooksLikePdf, which trivially checks that the payload begins with %PDF- and is at least 50 bytes long.

3. Unsandboxed Sink on macOS When the user clicks “Print” in the preview dialog, the payload is sent to the PrintBackendService. On macOS, features::kEnableOopPrintDriversSandbox is false by default (printing/printing_features.cc), meaning the PrintBackendService is launched completely unsandboxed (Sandbox::kNoSandbox). The payload eventually reaches PdfMetafileCg::RenderPage (printing/pdf_metafile_cg_mac.cc), where the bytes are parsed using Apple CoreGraphics APIs (CGPDFDocumentCreateWithProvider and CGContextDrawPDFPage). Exploiting memory corruption in the system CoreGraphics PDF parser would yield full unsandboxed code execution.

Potential Attacker Steps

(Note: These are suggested/potential steps to trigger the vulnerability, as our tooling agent does not currently have the ability to run code to provide a working proof-of-concept.)

  1. Gain code execution in a standard HTML renderer process (e.g., via a V8 or Blink vulnerability).
  2. Send a RequestPrintPreview IPC to the browser process, maliciously setting the is_modifiable flag to false in the parameters.
  3. Wait for the Print Preview WebUI to initialize and bind the mojom::PrintPreviewUI interface back to the compromised renderer.
  4. Call MetafileReadyForPrinting on the PrintPreviewUI interface, passing a shared memory region containing a crafted PDF designed to exploit a known or zero-day vulnerability in Apple’s CoreGraphics PDF parser.
  5. The attacker can use the compromised renderer to spoof the preview visual to look benign. When the user clicks “Print”, the malicious payload is processed by the unsandboxed PrintBackendService, resulting in a sandbox escape.

Suggested Fix

The browser should not trust the is_modifiable flag provided by the renderer in PrintViewManager::RequestPrintPreview and PrintViewManager::ShowScriptedPrintPreview.

Instead, the browser should securely determine this state itself. For example, it can override or validate the renderer’s flag using the process state:

params->is_modifiable = !render_frame_host->GetProcess()->IsPdf();

This is similar to how PrintViewManagerBase::CompleteScriptedPrint already securely determines if the document is modifiable.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


Results from 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