Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Chrome for iOS
DescriptionInsufficient policy enforcement in Chrome for iOS
ComponentChrome for iOS
Bug ClassLogic Error
Tracker504196549
Fix commite05e39e0b477 (chromium/src) +48/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
TEST_F
ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate_unittest.mm
modified

Files Changed

  • ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate.h
  • ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate.mm
  • ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate_unittest.mm
From e05e39e0b4777ca7ae5a72d53c5c4a87cf4afed3 Mon Sep 17 00:00:00 2001
From: Quentin Pubert <[email protected]>
Date: Mon, 27 Apr 2026 03:48:19 -0700
Subject: [PATCH] [iOS][ReaderMode] Fix Enterprise Data Controls bypass

Forward missing clipboard delegate methods (ShouldAllowPaste,
ShouldAllowCut, DidFinishClipboardRead) in ReaderModeWebStateDelegate to
the original tab's delegate. This prevents bypassing enterprise Data
Controls (DLP) policies when viewing a page in Reader Mode.

Bug: 504196549
Change-Id: Iab560fe556693895b48cb8033e4418f07ddf7584
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7793248
Auto-Submit: Quentin Pubert <[email protected]>
Commit-Queue: Quentin Pubert <[email protected]>
Reviewed-by: Elmehdi Rahmaoui <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1620985}
---

diff --git a/ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate.h b/ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate.h
index 10cc35690..6275fb3 100644
--- a/ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate.h
+++ b/ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate.h
@@ -50,6 +50,11 @@
       id<UIContextMenuInteractionCommitAnimating> animator) override;
   void ShouldAllowCopy(web::WebState* source,
                        base::OnceCallback<void(bool)> callback) override;
+  void ShouldAllowPaste(web::WebState* source,
+                        base::OnceCallback<void(bool)> callback) override;
+  void ShouldAllowCut(web::WebState* source,
+                      base::OnceCallback<void(bool)> callback) override;
+  void DidFinishClipboardRead(web::WebState* source) override;
 
  private:
   raw_ptr<web::WebState> original_web_state_ = nullptr;
diff --git a/ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate.mm b/ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate.mm
index b6e2dfbb..c9efb74 100644
--- a/ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate.mm
+++ b/ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate.mm
@@ -90,3 +90,19 @@
     base::OnceCallback<void(bool)> callback) {
   web_state_delegate_->ShouldAllowCopy(source, std::move(callback));
 }
+
+void ReaderModeWebStateDelegate::ShouldAllowPaste(
+    web::WebState* source,
+    base::OnceCallback<void(bool)> callback) {
+  web_state_delegate_->ShouldAllowPaste(source, std::move(callback));
+}
+
+void ReaderModeWebStateDelegate::ShouldAllowCut(
+    web::WebState* source,
+    base::OnceCallback<void(bool)> callback) {
+  web_state_delegate_->ShouldAllowCut(source, std::move(callback));
+}
+
+void ReaderModeWebStateDelegate::DidFinishClipboardRead(web::WebState* source) {
+  web_state_delegate_->DidFinishClipboardRead(source);
+}
diff --git a/ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate_unittest.mm b/ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate_unittest.mm
index 8e9d02b..f20e7ca 100644
--- a/ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate_unittest.mm
+++ b/ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate_unittest.mm
@@ -65,6 +65,15 @@
               ShouldAllowCopy,
               (web::WebState*, base::OnceCallback<void(bool)>),
               (override));
+  MOCK_METHOD(void,
+              ShouldAllowPaste,
+              (web::WebState*, base::OnceCallback<void(bool)>),
+              (override));
+  MOCK_METHOD(void,
+              ShouldAllowCut,
+              (web::WebState*, base::OnceCallback<void(bool)>),
+              (override));
+  MOCK_METHOD(void, DidFinishClipboardRead, (web::WebState*), (override));
 };
 
 }  // namespace
@@ -177,3 +186,21 @@
   EXPECT_CALL(mock_web_state_delegate_, ShouldAllowCopy(nullptr, testing::_));
   reader_mode_web_state_delegate_.ShouldAllowCopy(nullptr, base::DoNothing());
 }
+
+// Tests that ShouldAllowPaste is forwarded.
+TEST_F(ReaderModeWebStateDelegateTest, ShouldAllowPasteForwarded) {
+  EXPECT_CALL(mock_web_state_delegate_, ShouldAllowPaste(nullptr, testing::_));
+  reader_mode_web_state_delegate_.ShouldAllowPaste(nullptr, base::DoNothing());
+}
+
+// Tests that ShouldAllowCut is forwarded.
+TEST_F(ReaderModeWebStateDelegateTest, ShouldAllowCutForwarded) {
+  EXPECT_CALL(mock_web_state_delegate_, ShouldAllowCut(nullptr, testing::_));
+  reader_mode_web_state_delegate_.ShouldAllowCut(nullptr, base::DoNothing());
+}
+
+// Tests that DidFinishClipboardRead is forwarded.
+TEST_F(ReaderModeWebStateDelegateTest, DidFinishClipboardReadForwarded) {
+  EXPECT_CALL(mock_web_state_delegate_, DidFinishClipboardRead(nullptr));
+  reader_mode_web_state_delegate_.DidFinishClipboardRead(nullptr);
+}
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate_unittest.mm b/ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate_unittest.mm
index 8e9d02b..f20e7ca 100644
--- a/ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate_unittest.mm
+++ b/ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate_unittest.mm
@@ -65,6 +65,15 @@
               ShouldAllowCopy,
               (web::WebState*, base::OnceCallback<void(bool)>),
               (override));
+  MOCK_METHOD(void,
+              ShouldAllowPaste,
+              (web::WebState*, base::OnceCallback<void(bool)>),
+              (override));
+  MOCK_METHOD(void,
+              ShouldAllowCut,
+              (web::WebState*, base::OnceCallback<void(bool)>),
+              (override));
+  MOCK_METHOD(void, DidFinishClipboardRead, (web::WebState*), (override));
 };
 
 }  // namespace
@@ -177,3 +186,21 @@
   EXPECT_CALL(mock_web_state_delegate_, ShouldAllowCopy(nullptr, testing::_));
   reader_mode_web_state_delegate_.ShouldAllowCopy(nullptr, base::DoNothing());
 }
+
+// Tests that ShouldAllowPaste is forwarded.
+TEST_F(ReaderModeWebStateDelegateTest, ShouldAllowPasteForwarded) {
+  EXPECT_CALL(mock_web_state_delegate_, ShouldAllowPaste(nullptr, testing::_));
+  reader_mode_web_state_delegate_.ShouldAllowPaste(nullptr, base::DoNothing());
+}
+
+// Tests that ShouldAllowCut is forwarded.
+TEST_F(ReaderModeWebStateDelegateTest, ShouldAllowCutForwarded) {
+  EXPECT_CALL(mock_web_state_delegate_, ShouldAllowCut(nullptr, testing::_));
+  reader_mode_web_state_delegate_.ShouldAllowCut(nullptr, base::DoNothing());
+}
+
+// Tests that DidFinishClipboardRead is forwarded.
+TEST_F(ReaderModeWebStateDelegateTest, DidFinishClipboardReadForwarded) {
+  EXPECT_CALL(mock_web_state_delegate_, DidFinishClipboardRead(nullptr));
+  reader_mode_web_state_delegate_.DidFinishClipboardRead(nullptr);
+}
Loading diff…

Original Bug Report

reported by [email protected]

Potential iOS Enterprise Data Controls (DLP) bypass in Reader Mode via missing delegate overrides

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

Overview: The proxy ReaderModeWebStateDelegate on iOS fails to forward ShouldAllowPaste, ShouldAllowCut, and DidFinishClipboardRead methods to the original tab’s delegate. This causes these methods to fall back to the base web::WebStateDelegate implementations, which unconditionally allow the operation, bypassing enterprise Data Controls (DLP) policies and audit logging when viewing a page in Reader Mode.

Affected files:

  • ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate.mm
  • ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate.h

Estimated timestamp from git blame: 2025-11-03

Description

On iOS, when a user enters Reader Mode, ReaderModeTabHelper::CreateReaderModeContent creates a new web::WebState to display the distilled content. To ensure consistent behavior with the original tab, a proxy delegate, ReaderModeWebStateDelegate, is instantiated and attached to this new WebState. Its purpose is to forward relevant WebStateDelegate calls back to the original tab’s delegate (WebStateDelegateBrowserAgent).

While ReaderModeWebStateDelegate correctly overrides and forwards ShouldAllowCopy, it is missing overrides for three related clipboard methods crucial for Enterprise Data Controls:

  1. ShouldAllowPaste
  2. ShouldAllowCut
  3. DidFinishClipboardRead

Because these methods are not overridden in the proxy, any invocation falls back to the base implementations in web::WebStateDelegate (e.g., ios/web/web_state/web_state_delegate.mm:48). The base implementations for ShouldAllowPaste and ShouldAllowCut unconditionally allow the operation (executing std::move(callback).Run(true);), and DidFinishClipboardRead is an empty function.

Impact

Enterprise Data Controls (DLP policies) on iOS are enforced via WebStateDelegateBrowserAgent::ShouldAllowPaste, which delegates the decision to DataControlsTabHelper. By failing to forward the ShouldAllowPaste call, Reader Mode content completely bypasses enterprise policy enforcement.

If a user views a page subject to a DataControlsRules paste-destination policy (e.g., BLOCK or WARN) in Reader Mode:

  • Any programmatic or native paste operation is allowed without rule evaluation.
  • No BLOCK snackbar or WARN dialog is presented to the user.
  • No audit reporting events (MaybeReportDataControlsPaste) are generated because DidFinishClipboardRead is never forwarded to the browser agent.

Potential Exploitation Steps

An attacker could potentially trigger this bypass using the following steps:

  1. An enterprise administrator configures a Data Controls rule to BLOCK or WARN when pasting sensitive data to a specific domain (e.g., https://untrusted-site.example).
  2. The user copies sensitive enterprise data to the iOS system clipboard.
  3. The user navigates to an article on https://untrusted-site.example and activates Reader Mode.
  4. The iOS DOM Distiller preserves certain tags, including iframe elements (e.g., embedded YouTube videos or other supported frames).
  5. A malicious script executing within a surviving iframe attempts to read the clipboard via navigator.clipboard.readText().
  6. This triggers ClipboardJavaScriptFeature::HandleClipboardRequest, which calls web_state->GetDelegate()->ShouldAllowPaste().
  7. The call resolves to the proxy delegate, falls back to the base web::WebStateDelegate, and unconditionally returns true.
  8. The sensitive data is returned to the malicious script, completely bypassing the Data Controls policy and audit logging.

(Note: These are suggested steps based on static analysis; our tooling agent does not have the ability to run code to confirm a working exploit).

Update ReaderModeWebStateDelegate (ios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate.h/mm) to override ShouldAllowPaste, ShouldAllowCut, and DidFinishClipboardRead. These overrides should forward the calls to the web_state_delegate_ instance variable, mirroring the implementation of ShouldAllowCopy.

For example:

void ReaderModeWebStateDelegate::ShouldAllowPaste(
    web::WebState* source,
    base::OnceCallback<void(bool)> callback) {
  web_state_delegate_->ShouldAllowPaste(source, std::move(callback));
}

Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646


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