CVE-2026-11302
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fios/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.hios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate.mmios/chrome/browser/reader_mode/model/reader_mode_web_state_delegate_unittest.mm
Patch
From e05e39e0b4777ca7ae5a72d53c5c4a87cf4afed3 Mon Sep 17 00:00:00 2001
From: Quentin Pubert <qpubert@google.com>
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 <qpubert@google.com>
Commit-Queue: Quentin Pubert <qpubert@google.com>
Reviewed-by: Elmehdi Rahmaoui <erahmaoui@google.com>
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);
+}
Regression Test / PoC
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);
+}
Original Bug Report
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.mmios/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:
ShouldAllowPasteShouldAllowCutDidFinishClipboardRead
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 becauseDidFinishClipboardReadis never forwarded to the browser agent.
Potential Exploitation Steps
An attacker could potentially trigger this bypass using the following steps:
- An enterprise administrator configures a Data Controls rule to
BLOCKorWARNwhen pasting sensitive data to a specific domain (e.g.,https://untrusted-site.example). - The user copies sensitive enterprise data to the iOS system clipboard.
- The user navigates to an article on
https://untrusted-site.exampleand activates Reader Mode. - The iOS DOM Distiller preserves certain tags, including
iframeelements (e.g., embedded YouTube videos or other supported frames). - A malicious script executing within a surviving
iframeattempts to read the clipboard vianavigator.clipboard.readText(). - This triggers
ClipboardJavaScriptFeature::HandleClipboardRequest, which callsweb_state->GetDelegate()->ShouldAllowPaste(). - The call resolves to the proxy delegate, falls back to the base
web::WebStateDelegate, and unconditionally returnstrue. - 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).
Recommended Fix
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.