Medium chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Mobile
DescriptionMissing authorization in Mobile
ComponentMobile
Sub-componentiOS
Bug ClassAuthorization Error
Tracker505186109
Fix commit52fc98ec96e0 (chromium/src) +91/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
ios/chrome/browser/print/coordinator/print_coordinator_impl.mm
modified
TEST_F
ios/chrome/browser/web/model/print/print_java_script_feature_unittest.mm
modified

Files Changed

  • ios/chrome/browser/print/coordinator/print_coordinator_impl.mm
  • ios/chrome/browser/print/coordinator/print_coordinator_impl.swift
  • ios/chrome/browser/web/model/print/print_java_script_feature_unittest.mm
  • ios/chrome/browser/web/model/print/BUILD.gn
  • ios/chrome/browser/web/model/print/print_java_script_feature.mm
From 52fc98ec96e0a537026ea452640075b1080adc58 Mon Sep 17 00:00:00 2001
From: Daniel White <[email protected]>
Date: Fri, 11 Sep 2026 10:22:47 -0700
Subject: [PATCH] [iOS] Ignore window.print() from background tabs

UIPrintInteractionController is a process-wide singleton, so a
window.print() call from a background tab would overwrite the delegate,
renderer and parent view controller of any print sheet already on screen
for the foreground tab.

Drop window.print() messages when the originating WebState is not
visible, and have PrintCoordinator ignore new requests while the shared
controller already has a delegate. Clear the controller's delegate,
renderer and item on completion and on -stop so the WKWebView-backed
renderer is released and subsequent requests see a clean state.

Bug: 505186109
Change-Id: I887e2fab0ac182fe587f6888427337e7c5afb9dd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8384938
Reviewed-by: Sylvain Defresne <[email protected]>
Commit-Queue: Daniel White <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1696394}
---
diff --git a/ios/chrome/browser/print/coordinator/print_coordinator_impl.mm b/ios/chrome/browser/print/coordinator/print_coordinator_impl.mm
index 7dbac8a..988b253b 100644
--- a/ios/chrome/browser/print/coordinator/print_coordinator_impl.mm
+++ b/ios/chrome/browser/print/coordinator/print_coordinator_impl.mm
@@ -29,6 +29,14 @@
 #pragma mark - ChromeCoordinator
 
 - (void)stop {
+  UIPrintInteractionController* printInteractionController =
+      [UIPrintInteractionController sharedPrintController];
+  if (printInteractionController.delegate == self) {
+    printInteractionController.delegate = nil;
+    printInteractionController.printPageRenderer = nil;
+    printInteractionController.printingItem = nil;
+    printInteractionController.printInfo = nil;
+  }
   self.defaultBaseViewController = nil;
 }
 
@@ -82,10 +90,16 @@
   CHECK_EQ((renderer ? 1 : 0) + (item ? 1 : 0), 1);
   CHECK(title);
   CHECK(baseViewController);
-  self.defaultBaseViewController = baseViewController;
-  base::RecordAction(base::UserMetricsAction("MobilePrintMenuAirPrint"));
   UIPrintInteractionController* printInteractionController =
       [UIPrintInteractionController sharedPrintController];
+  if (printInteractionController.delegate) {
+    // The shared print controller is already presenting on behalf of this or
+    // another coordinator. Ignore this request rather than mutating the state
+    // of the in-progress session.
+    return;
+  }
+  self.defaultBaseViewController = baseViewController;
+  base::RecordAction(base::UserMetricsAction("MobilePrintMenuAirPrint"));
   printInteractionController.delegate = self;
 
   UIPrintInfo* printInfo = [UIPrintInfo printInfo];
@@ -104,6 +118,10 @@
           DLOG(ERROR) << "Air printing error: "
                       << base::SysNSStringToUTF8(error.description);
         }
+        controller.delegate = nil;
+        controller.printPageRenderer = nil;
+        controller.printingItem = nil;
+        controller.printInfo = nil;
       }];
 }
 
diff --git a/ios/chrome/browser/print/coordinator/print_coordinator_impl.swift b/ios/chrome/browser/print/coordinator/print_coordinator_impl.swift
index 65fbfebe..fddff3f 100644
--- a/ios/chrome/browser/print/coordinator/print_coordinator_impl.swift
+++ b/ios/chrome/browser/print/coordinator/print_coordinator_impl.swift
@@ -27,6 +27,13 @@
 
   @objc override func stop() {
     MainActor.assumeIsolated {
+      let printInteractionController = UIPrintInteractionController.shared
+      if printInteractionController.delegate === self {
+        printInteractionController.delegate = nil
+        printInteractionController.printPageRenderer = nil
+        printInteractionController.printingItem = nil
+        printInteractionController.printInfo = nil
+      }
       defaultBaseViewController = nil
     }
   }
@@ -71,9 +78,15 @@
   ) {
     // Only one item must be passed.
     assert((renderer != nil) != (item != nil))
+    let printInteractionController = UIPrintInteractionController.shared
+    if printInteractionController.delegate != nil {
+      // The shared print controller is already presenting on behalf of this or
+      // another coordinator. Ignore this request rather than mutating the state
+      // of the in-progress session.
+      return
+    }
     self.defaultBaseViewController = baseViewController
     base.swift.RecordUserMetricsAction("MobilePrintMenuAirPrint")
-    let printInteractionController = UIPrintInteractionController.shared
     printInteractionController.delegate = self
 
     let printInfo = UIPrintInfo.printInfo()
@@ -90,6 +103,10 @@
         if let error = error {
           NSLog("Air printing error: %@", error.localizedDescription)
         }
+        controller.delegate = nil
+        controller.printPageRenderer = nil
+        controller.printingItem = nil
+        controller.printInfo = nil
       })
   }
 }
diff --git a/ios/chrome/browser/web/model/print/print_java_script_feature.mm b/ios/chrome/browser/web/model/print/print_java_script_feature.mm
index 8bda5bb6..f460ba6 100644
--- a/ios/chrome/browser/web/model/print/print_java_script_feature.mm
+++ b/ios/chrome/browser/web/model/print/print_java_script_feature.mm
@@ -36,6 +36,15 @@
     const web::ScriptMessage& message) {
   DCHECK(web_state);
 
+  // TODO(crbug.com/560132282): Extend this condition to cover pre-rendered tabs
+  // as well.
+  if (!web_state->IsVisible()) {
+    // Ignore window.print() calls from background tabs. The system print
+    // controller is process-wide and showing it for a hidden WebState would
+    // present over (or replace) the foreground tab's UI.
+    return;
+  }
+
   if (!message.is_main_frame() && !message.is_user_interacting()) {
     // Ignore non user-initiated window.print() calls from iframes, to prevent
     // abusive behavior from web sites.
diff --git a/ios/chrome/browser/web/model/print/print_java_script_feature_unittest.mm b/ios/chrome/browser/web/model/print/print_java_script_feature_unittest.mm
index 26a63219..5cce02c 100644
--- a/ios/chrome/browser/web/model/print/print_java_script_feature_unittest.mm
+++ b/ios/chrome/browser/web/model/print/print_java_script_feature_unittest.mm
@@ -8,6 +8,7 @@
 #import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
 #import "ios/chrome/browser/web/model/print/print_handler.h"
 #import "ios/chrome/browser/web/model/print/print_tab_helper.h"
+#import "ios/web/public/js_messaging/script_message.h"
 #import "ios/web/public/test/fakes/fake_web_client.h"
 #import "ios/web/public/test/scoped_testing_web_client.h"
 #import "ios/web/public/test/web_state_test_util.h"
@@ -15,6 +16,7 @@
 #import "ios/web/public/test/web_view_interaction_test_util.h"
 #import "ios/web/public/web_state.h"
 #import "testing/platform_test.h"
+#import "url/origin.h"
 
 namespace {
 const char kButtonPageHtml[] =
@@ -72,6 +74,15 @@
   }
   web::WebState* web_state() { return web_state_.get(); }
 
+  // Delivers a script message directly to the feature, simulating a
+  // window.print() call from a frame with the given properties.
+  void DeliverPrintMessage(bool is_user_interacting, bool is_main_frame) {
+    web::ScriptMessage message(std::make_unique<base::Value>(),
+                               is_user_interacting, is_main_frame,
+                               /*request_url=*/std::nullopt, url::Origin());
+    feature_.ScriptMessageReceived(web_state(), message);
+  }
+
   web::ScopedTestingWebClient web_client_;
   web::WebTaskEnvironment task_environment_;
   std::unique_ptr<TestProfileIOS> profile_;
@@ -83,8 +94,39 @@
 TEST_F(PrintJavaScriptFeatureTest, PrintInvoked) {
   ASSERT_FALSE(printer_.printInvoked);
 
+  web_state()->WasShown();
   web::test::LoadHtml(base::SysUTF8ToNSString(kButtonPageHtml), web_state());
 
   ASSERT_TRUE(web::test::TapWebViewElementWithId(web_state(), "button"));
   EXPECT_TRUE(printer_.printInvoked);
 }
+
+// Tests that window.print() from the main frame of a visible WebState is
+// forwarded to the printer.
+TEST_F(PrintJavaScriptFeatureTest, PrintFromVisibleMainFrame) {
+  ASSERT_FALSE(printer_.printInvoked);
+
+  web_state()->WasShown();
+  DeliverPrintMessage(/*is_user_interacting=*/false, /*is_main_frame=*/true);
+  EXPECT_TRUE(printer_.printInvoked);
+}
+
+// Tests that window.print() from the main frame of a hidden WebState is
+// ignored.
+TEST_F(PrintJavaScriptFeatureTest, PrintIgnoredFromHiddenMainFrame) {
+  ASSERT_FALSE(printer_.printInvoked);
+
+  web_state()->WasHidden();
+  DeliverPrintMessage(/*is_user_interacting=*/false, /*is_main_frame=*/true);
+  EXPECT_FALSE(printer_.printInvoked);
+}
+
+// Tests that window.print() from a subframe of a hidden WebState is ignored
+// even when the user is interacting.
+TEST_F(PrintJavaScriptFeatureTest, PrintIgnoredFromHiddenSubframe) {
+  ASSERT_FALSE(printer_.printInvoked);
+
+  web_state()->WasHidden();
+  DeliverPrintMessage(/*is_user_interacting=*/true, /*is_main_frame=*/false);
+  EXPECT_FALSE(printer_.printInvoked);
+}
diff --git a/ios/chrome/browser/web/model/print/BUILD.gn b/ios/chrome/browser/web/model/print/BUILD.gn
index c189526e..0e7767bb 100644
--- a/ios/chrome/browser/web/model/print/BUILD.gn
+++ b/ios/chrome/browser/web/model/print/BUILD.gn
@@ -41,8 +41,10 @@
     "//ios/chrome/browser/shared/model/prefs:pref_names",
     "//ios/chrome/browser/shared/model/profile/test",
     "//ios/chrome/test/fakes",
+    "//ios/web/public/js_messaging",
     "//ios/web/public/test",
     "//testing/gtest",
+    "//url",
   ]
 }
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ios/chrome/browser/web/model/print/print_java_script_feature_unittest.mm b/ios/chrome/browser/web/model/print/print_java_script_feature_unittest.mm
index 26a63219..5cce02c 100644
--- a/ios/chrome/browser/web/model/print/print_java_script_feature_unittest.mm
+++ b/ios/chrome/browser/web/model/print/print_java_script_feature_unittest.mm
@@ -8,6 +8,7 @@
 #import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
 #import "ios/chrome/browser/web/model/print/print_handler.h"
 #import "ios/chrome/browser/web/model/print/print_tab_helper.h"
+#import "ios/web/public/js_messaging/script_message.h"
 #import "ios/web/public/test/fakes/fake_web_client.h"
 #import "ios/web/public/test/scoped_testing_web_client.h"
 #import "ios/web/public/test/web_state_test_util.h"
@@ -15,6 +16,7 @@
 #import "ios/web/public/test/web_view_interaction_test_util.h"
 #import "ios/web/public/web_state.h"
 #import "testing/platform_test.h"
+#import "url/origin.h"
 
 namespace {
 const char kButtonPageHtml[] =
@@ -72,6 +74,15 @@
   }
   web::WebState* web_state() { return web_state_.get(); }
 
+  // Delivers a script message directly to the feature, simulating a
+  // window.print() call from a frame with the given properties.
+  void DeliverPrintMessage(bool is_user_interacting, bool is_main_frame) {
+    web::ScriptMessage message(std::make_unique<base::Value>(),
+                               is_user_interacting, is_main_frame,
+                               /*request_url=*/std::nullopt, url::Origin());
+    feature_.ScriptMessageReceived(web_state(), message);
+  }
+
   web::ScopedTestingWebClient web_client_;
   web::WebTaskEnvironment task_environment_;
   std::unique_ptr<TestProfileIOS> profile_;
@@ -83,8 +94,39 @@
 TEST_F(PrintJavaScriptFeatureTest, PrintInvoked) {
   ASSERT_FALSE(printer_.printInvoked);
 
+  web_state()->WasShown();
   web::test::LoadHtml(base::SysUTF8ToNSString(kButtonPageHtml), web_state());
 
   ASSERT_TRUE(web::test::TapWebViewElementWithId(web_state(), "button"));
   EXPECT_TRUE(printer_.printInvoked);
 }
+
+// Tests that window.print() from the main frame of a visible WebState is
+// forwarded to the printer.
+TEST_F(PrintJavaScriptFeatureTest, PrintFromVisibleMainFrame) {
+  ASSERT_FALSE(printer_.printInvoked);
+
+  web_state()->WasShown();
+  DeliverPrintMessage(/*is_user_interacting=*/false, /*is_main_frame=*/true);
+  EXPECT_TRUE(printer_.printInvoked);
+}
+
+// Tests that window.print() from the main frame of a hidden WebState is
+// ignored.
+TEST_F(PrintJavaScriptFeatureTest, PrintIgnoredFromHiddenMainFrame) {
+  ASSERT_FALSE(printer_.printInvoked);
+
+  web_state()->WasHidden();
+  DeliverPrintMessage(/*is_user_interacting=*/false, /*is_main_frame=*/true);
+  EXPECT_FALSE(printer_.printInvoked);
+}
+
+// Tests that window.print() from a subframe of a hidden WebState is ignored
+// even when the user is interacting.
+TEST_F(PrintJavaScriptFeatureTest, PrintIgnoredFromHiddenSubframe) {
+  ASSERT_FALSE(printer_.printInvoked);
+
+  web_state()->WasHidden();
+  DeliverPrintMessage(/*is_user_interacting=*/true, /*is_main_frame=*/false);
+  EXPECT_FALSE(printer_.printInvoked);
+}
Loading diff…

Original Bug Report

reported by [email protected]

State Corruption in UIPrintInteractionController leads to Spoofing and DoS

Flapjack, 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 go/chrome-ai-generated-security-bugs-faq for more information.

Overview: The PrintCoordinatorImpl unconditionally overwrites the shared UIPrintInteractionController singleton properties without verifying if a print session is active. A background tab can trigger a print request via window.print(), hijacking an ongoing print dialog in a foreground tab. This leads to potential print content spoofing, a browser crash (DoS) due to a window hierarchy mismatch, and a persistent memory leak.

Affected files:

  • ios/chrome/browser/print/coordinator/print_coordinator_impl.mm

Estimated timestamp from git blame: 2026-01-16

Vulnerability Mechanism

In iOS Chrome, printing is managed using the UIPrintInteractionController system singleton ([UIPrintInteractionController sharedPrintController]).

When a print job is initiated, PrintCoordinatorImpl::printRenderer:orItem:withTitle:baseViewController: immediately and unconditionally modifies the singleton’s properties (delegate, printInfo, printPageRenderer, and printingItem) before calling presentAnimated:.

Simultaneously, PrintJavaScriptFeature::ScriptMessageReceived handles window.print() calls from web content. It checks !message.is_main_frame() && !message.is_user_interacting() to block background iframes. However, a background main frame satisfies message.is_main_frame() and is allowed to proceed without user interaction.

Because the UIPrintInteractionController state is shared across the entire application (even across multiple windows in an iPad split-view scenario), a background tab can overwrite the properties of an active, user-initiated print dialog currently on screen.

Potential Security Impacts

  1. Print Content Spoofing: If a user is reviewing a legitimate document in the system print dialog, and a background tab fires window.print(), the printPageRenderer on the singleton is replaced. UIKit does not snapshot the renderer at presentation time; it reads from it when the spooler processes the job. If the user taps “Print”, they will unknowingly print the attacker’s fraudulent content instead of the expected document.

  2. Denial of Service (Browser Crash): When the background tab initiates printing, its PrintCoordinatorImpl sets itself as the singleton’s delegate. If the background tab belongs to a different window (e.g., in iPad split-view), the delegate’s printInteractionControllerParentViewController: method returns a baseViewController from the background window hierarchy. UIKit attempts to present the print interaction controller (or reconcile an existing presentation) using a view controller from a different, inactive window hierarchy, triggering an NSInternalInconsistencyException that crashes the Chrome process.

  3. Memory Leak: The completion handler passed to presentAnimated:completionHandler: does not clear the singleton’s properties after the dialog is dismissed. The UIPrintInteractionController singleton indefinitely retains the last used UIPrintPageRenderer, which retains the WKWebView. This causes a substantial memory leak of the web view after every print job.

Potential Steps to Trigger

(Note: These are suggested steps based on static analysis, as our tooling cannot execute a live proof of concept yet)

  1. An attacker lures a user to a malicious site, and instructs them to open a legitimate, sensitive site (e.g., a bank statement) in a second window (split-view) or a foreground tab.
  2. The user initiates a print action on the legitimate site.
  3. The system print dialog is displayed, showing the legitimate document.
  4. The malicious site, using a timer or visibility API listener, executes window.print() in its main frame.
  5. The background print request bypasses interaction checks and updates the UIPrintInteractionController singleton with the malicious site’s content and delegate.
  6. Either the browser crashes immediately due to the delegate/hierarchy mismatch, or the user proceeds to print, generating a spoofed physical/PDF document containing the attacker’s content.

Recommendation

  1. State Checking: Before overwriting properties on [UIPrintInteractionController sharedPrintController], verify if a print session is already active (e.g., check printInteractionController.delegate != nil). If a session is active, ignore or queue the background request.
  2. Restrict Background Printing: Update PrintJavaScriptFeature::ScriptMessageReceived to check web_state->IsVisible() to ensure only the active, visible tab can trigger printing.
  3. Cleanup: In the completionHandler of presentAnimated:, set delegate = nil, printPageRenderer = nil, and printingItem = nil to prevent memory leaks.

Evaluated with Chrome root at commit: 4a3e9db74111a3c6c4b3acfd70050a05077cf27a


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