Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Reader Mode
DescriptionInsufficient validation of untrusted input in Reader Mode
ComponentReader Mode
Bug ClassLogic Error
Tracker502502017
Fix commitbd2a1c80bb4a (chromium/src) +31/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • chrome/android/java/src/org/chromium/chrome/browser/dom_distiller/ReaderModeManager.java
  • chrome/android/junit/src/org/chromium/chrome/browser/dom_distiller/ReaderModeManagerTest.java
From bd2a1c80bb4a02b6ffdfaac27ea3f28760aa8145 Mon Sep 17 00:00:00 2001
From: Evan Luo <[email protected]>
Date: Tue, 14 Apr 2026 15:29:05 -0700
Subject: [PATCH] [reader mode] Fix incognito tab injection from reader mode intent extra

Untrusted apps could bypass the normal checks for launch intents since
the ReaderModeManager.isReaderModeCreatedIntent was not checking for
wasIntentSenderChrome. External apps shouldn't be able to launch tabs
in incognito, with this fix, it launches properly in non-incognito.

Screen recording:
http://screencast/cast/NTMzMjExNzk2ODkxMjM4NHxmZjFiYWMzMy00Nw

Bug: 502502017
Change-Id: Ifa792bc48d3ab7ddc5f10be130fdfbcbec318234
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7763202
Reviewed-by: Brandon Wylie <[email protected]>
Commit-Queue: Evan Luo <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1614752}
---

diff --git a/chrome/android/java/src/org/chromium/chrome/browser/dom_distiller/ReaderModeManager.java b/chrome/android/java/src/org/chromium/chrome/browser/dom_distiller/ReaderModeManager.java
index 41eb73f..4eb8733b 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/dom_distiller/ReaderModeManager.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/dom_distiller/ReaderModeManager.java
@@ -1050,10 +1050,13 @@
 
     /**
      * Determine if Reader Mode created the intent for a tab being created.
+     *
      * @param intent The Intent creating a new tab.
      * @return True whether the intent was created by Reader Mode.
      */
     public static boolean isReaderModeCreatedIntent(Intent intent) {
+        // Ensure that the intent is from a trusted intent.
+        if (!IntentHandler.wasIntentSenderChrome(intent)) return false;
         int readerParentId =
                 IntentUtils.safeGetIntExtra(
                         intent, ReaderModeManager.EXTRA_READER_MODE_PARENT, Tab.INVALID_TAB_ID);
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/dom_distiller/ReaderModeManagerTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/dom_distiller/ReaderModeManagerTest.java
index 6fcf4d7..1631342f 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/dom_distiller/ReaderModeManagerTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/dom_distiller/ReaderModeManagerTest.java
@@ -19,6 +19,7 @@
 import static org.mockito.Mockito.when;
 
 import android.app.Activity;
+import android.content.Intent;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.os.Looper;
@@ -38,7 +39,9 @@
 import org.robolectric.Shadows;
 
 import org.chromium.base.Callback;
+import org.chromium.base.ContextUtils;
 import org.chromium.base.FeatureOverrides;
+import org.chromium.base.IntentUtils;
 import org.chromium.base.UnownedUserDataHost;
 import org.chromium.base.UserDataHost;
 import org.chromium.base.supplier.OneshotSupplierImpl;
@@ -796,6 +799,31 @@
         assertFalse(ReaderModeManager.shouldUseReaderModeMessages(mTab));
     }
 
+    @Test
+    @Feature("ReaderMode")
+    public void testIsReaderModeCreatedIntent_NotChrome() {
+        Intent intent = new Intent();
+        intent.putExtra(ReaderModeManager.EXTRA_READER_MODE_PARENT, 1);
+
+        assertFalse(
+                "Untrusted intent should not be considered Reader Mode created",
+                ReaderModeManager.isReaderModeCreatedIntent(intent));
+    }
+
+    @Test
+    @Feature("ReaderMode")
+    public void testIsReaderModeCreatedIntent_FromChrome() {
+        Intent intent = new Intent();
+        intent.putExtra(ReaderModeManager.EXTRA_READER_MODE_PARENT, 1);
+
+        intent.setPackage(ContextUtils.getApplicationContext().getPackageName());
+        IntentUtils.addTrustedIntentExtras(intent);
+
+        assertTrue(
+                "Intent from Chrome should be considered Reader Mode created",
+                ReaderModeManager.isReaderModeCreatedIntent(intent));
+    }
+
     private NavigationEntry createNavigationEntry(int index, GURL url) {
         return new NavigationEntry(
                 index, url, url, url, "", null, 0, 0, /* isInitialEntry= */ false);
Loading diff…

Original Bug Report

reported by [email protected]

Potential Incognito tab injection via spoofed Reader Mode intent extra

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.

Overview: An external Android application can bypass intent security checks by spoofing the EXTRA_READER_MODE_PARENT extra in a VIEW intent. This causes Chrome to process the untrusted intent through an internal Reader Mode code path, skipping launchUrlFromExternalApp. If the user currently has the Incognito tab model open, this allows the attacker to silently open an arbitrary URL in the user’s Incognito session.

Affected files:

  • chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
  • chrome/android/java/src/org/chromium/chrome/browser/dom_distiller/ReaderModeManager.java

Estimated timestamp from git blame: 2025-06-30

Summary

There is a potential vulnerability in how Chrome for Android handles incoming intents in ChromeTabbedActivity. By appending a specific extra (org.chromium.chrome.browser.dom_distiller.EXTRA_READER_MODE_PARENT), an untrusted external application can trick Chrome into treating the intent as an internal Reader Mode transition. This bypasses standard external intent routing and allows the attacker to inject a URL into the currently active Tab Model, which can bypass the restriction against external apps opening URLs in Incognito mode.

Technical Details

When Chrome receives an android.intent.action.VIEW intent, it is eventually processed by ChromeTabbedActivity.launchIntent.

This method first checks if the intent originated from a trusted Chrome sender (IntentHandler.wasIntentSenderChrome(intent)). If it did not, execution skips the trusted block.

However, execution then proceeds to the following check:

if (ReaderModeManager.isEnabled() && ReaderModeManager.isReaderModeCreatedIntent(intent)) {

ReaderModeManager.isReaderModeCreatedIntent(intent) simply checks for the presence of the EXTRA_READER_MODE_PARENT extra and ensures it is not equal to Tab.INVALID_TAB_ID. It does not verify the sender of the intent.

If the extra is present, the code executes:

return getCurrentTabCreator().createNewTab(...);

getCurrentTabCreator() returns the TabCreator for whichever Tab Model (Standard or Incognito) is currently active on the screen. This completely bypasses the intended fallback behavior at the end of the method:

Tab tab = getTabCreator(false).launchUrlFromExternalApp(...);

launchUrlFromExternalApp is the secure chokepoint for external intents and contains an explicit assertion (assert !mIncognito) to prevent external apps from opening tabs in Incognito.

Impact

If a user is actively browsing in an Incognito tab, or switches away from Chrome while an Incognito tab is active, an attacker application can send a spoofed intent that silently opens an arbitrary URL within that Incognito session. This violates the security boundary that prevents untrusted external applications from interacting with the user’s private browsing session.

Potential Reproduction Steps

Note: These steps are suggested based on code analysis, as a working Proof of Concept has not yet been executed.

  1. Open Chrome on Android and switch to the Incognito tab model (e.g., open a new Incognito tab).
  2. Press the home button to background Chrome, leaving Incognito as the active model.
  3. Using an unprivileged app or adb, send a VIEW intent with the spoofed extra:
    adb shell am start -n com.android.chrome/org.chromium.chrome.browser.ChromeTabbedActivity \
      -a android.intent.action.VIEW \
      -d "https://example.com" \
      --ei org.chromium.chrome.browser.dom_distiller.EXTRA_READER_MODE_PARENT 0
    
  4. Observe that Chrome opens https://example.com in the Incognito profile.

Suggested Fix

Update the logic to ensure that Reader Mode intents are only processed if they originate from Chrome. This can be done by modifying ChromeTabbedActivity.launchIntent to check the sender, or by updating ReaderModeManager.isReaderModeCreatedIntent:

public static boolean isReaderModeCreatedIntent(Intent intent) {
    if (!IntentHandler.wasIntentSenderChrome(intent)) return false;
    int readerParentId =
            IntentUtils.safeGetIntExtra(
                    intent, ReaderModeManager.EXTRA_READER_MODE_PARENT, Tab.INVALID_TAB_ID);
    return readerParentId != Tab.INVALID_TAB_ID;
}

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


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