Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in Document Picture-in-Picture
DescriptionIncorrect security UI in Document Picture-in-Picture
ComponentDocument Picture-in-Picture
Bug ClassLogic Error
Tracker514072867
Fix commita973f1e01606 (chromium/src) +130/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
chrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/PopupCreatorImpl.java
modified
if
chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java
modified

Files Changed

  • chrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/PopupCreatorImpl.java
  • chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java
  • chrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java
From a973f1e01606c63519e867535192ca0abbbaf62f Mon Sep 17 00:00:00 2001
From: Phil Yan <[email protected]>
Date: Thu, 28 May 2026 15:57:02 -0700
Subject: [PATCH] [Doc-PiP] Prevent origin spoofing via opener navigation race

An attacker can request a Document PiP window on Android and
immediately navigate the opener window. Due to the asynchronous
startup sequence of DocumentPictureInPictureActivity, there is a
100-500ms delay before the native WebContentsObserver is
registered. This causes the browser to miss the navigation and
display the navigated victim's origin in the trusted PiP header.

To address this:
1. Register the native observer early inside AddNewContents. This
   ensures opener navigations are caught immediately from the
   moment the PiP window is requested.
2. Pass the opener's serialized origin at launch time in the intent
   and verify it in performPreInflationStartup() and onStart(). On
   mismatch, close the window. Using serialized origin prevents
   Binder crashes on large URLs and supports opaque origin
   verification.
3. Make the native registration in OnActivityStart completely
   test-only (onActivityStartForTesting).

Bug: 514072867
Fix: 514072867
Change-Id: Id98308652cdf4c0fb2dea6e63b25b13fbffdcdc5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7876369
Commit-Queue: Phil Yan <[email protected]>
Reviewed-by: Frank Liberato <[email protected]>
Reviewed-by: Wenyu Fu <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1638015}
---

diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/PopupCreatorImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/PopupCreatorImpl.java
index ffaac6a..a75d5a2 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/PopupCreatorImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/app/tab_activity_glue/PopupCreatorImpl.java
@@ -55,6 +55,8 @@
 import org.chromium.ui.display.DisplayUtil;
 import org.chromium.ui.insets.InsetObserver;
 import org.chromium.ui.insets.WindowInsetsUtils;
+import org.chromium.url.GURL;
+import org.chromium.url.Origin;
 
 /** Handles launching new popup windows as CCTs and Document Picture-in-Picture windows. */
 @NullMarked
@@ -502,6 +504,17 @@
         intent.putExtra(
                 DocumentPictureInPictureActivity.WINDOW_OPTIONS_KEY, windowOptions.toBundle());
 
+        // Record the opener's origin at the time of the request to prevent origin spoofing
+        // if the opener navigates before the Activity completes its launch.
+        WebContents opener = webContents.getDocumentPictureInPictureOpener();
+        if (opener != null) {
+            GURL openerUrl = opener.getLastCommittedUrl();
+            Origin openerOrigin = Origin.create(openerUrl != null ? openerUrl : GURL.emptyGURL());
+            intent.putExtra(
+                    DocumentPictureInPictureActivity.INITIAL_OPENER_ORIGIN_KEY,
+                    openerOrigin.toString());
+        }
+
         intent.setAction(Intent.ACTION_VIEW);
 
         return intent;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java
index 9fa3c4e9..96e88dd 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivity.java
@@ -81,6 +81,7 @@
 import org.chromium.ui.display.DisplayUtil;
 import org.chromium.ui.modaldialog.ModalDialogManager;
 import org.chromium.url.GURL;
+import org.chromium.url.Origin;
 
 @NullMarked
 public class DocumentPictureInPictureActivity extends AsyncInitializationActivity
@@ -92,6 +93,8 @@
             "org.chromium.chrome.browser.media.DocumentPictureInPicture.WebContents";
     public static final String WINDOW_OPTIONS_KEY =
             "org.chromium.chrome.browser.media.DocumentPictureInPicture.WindowOptions";
+    public static final String INITIAL_OPENER_ORIGIN_KEY =
+            "org.chromium.chrome.browser.media.DocumentPictureInPicture.InitialOpenerOrigin";
     private static final String IS_FROM_ACTIVITY_RECREATION_KEY =
             "org.chromium.chrome.browser.media.DocumentPictureInPicture.IsFromActivityRecreation";
     private WebContents mWebContents;
@@ -155,6 +158,11 @@
         }
         mParentWebContents = parentWebContents;
 
+        if (!verifyOpenerOrigin(intent, parentWebContents)) {
+            finish();
+            return;
+        }
+
         Bundle windowOptionsBundle =
                 getWindowOptionsBundleFromInstanceStateOrIntent(intent, savedInstanceState);
         if (windowOptionsBundle == null) {
@@ -207,9 +215,9 @@
         super.onStart();
         assert isContentsInitialized();
 
-        if (!mIsFromActivityRecreation) {
-            DocumentPictureInPictureActivityJni.get()
-                    .onActivityStart(mParentWebContents, mWebContents);
+        if (!verifyOpenerOrigin(getIntent(), mParentWebContents)) {
+            finish();
+            return;
         }
 
         mInitiatorTabObserver =
@@ -832,6 +840,48 @@
     }
 
     /**
+     * Enters Picture-in-Picture mode for testing. This is intended for test environments that
+     * launch the Activity directly.
+     */
+    public static void onActivityStartForTesting(
+            WebContents parentWebContents, WebContents webContents) {
+        DocumentPictureInPictureActivityJni.get()
+                .onActivityStartForTesting(parentWebContents, webContents); // IN-TEST
+    }
+
+    /**
+     * Verifies that the current opener's origin matches the origin captured when the PiP window was
+     * requested. This protects against a race condition where the opener window navigates during
+     * the asynchronous Activity startup.
+     *
+     * @param intent The launch intent.
+     * @param parentWebContents The opener's WebContents.
+     * @return True if the origins match, or if verification is skipped; false on mismatch.
+     */
+    private boolean verifyOpenerOrigin(Intent intent, WebContents parentWebContents) {
+        if (mIsFromActivityRecreation) {
+            return true; // Already verified on initial startup.
+        }
+        final String initialOpenerOriginStr = intent.getStringExtra(INITIAL_OPENER_ORIGIN_KEY);
+        if (initialOpenerOriginStr == null) {
+            Log.e(TAG, "No initial opener origin in intent! Finishing.");
+            return false;
+        }
+        final GURL currentOpenerUrl = parentWebContents.getLastCommittedUrl();
+        final String currentOpenerOriginStr = Origin.create(currentOpenerUrl).toString();
+        if (!initialOpenerOriginStr.equals(currentOpenerOriginStr)) {
+            Log.e(
+                    TAG,
+                    "Opener origin mismatch! Initial: "
+                            + initialOpenerOriginStr
+                            + ", Current: "
+                            + currentOpenerOriginStr);
+            return false;
+        }
+        return true;
+    }
+
+    /**
      * Sets the parent WebContents directly on this instance for testing. Use this in unit tests
      * where the activity is created without running the full startup flow.
      */
@@ -854,7 +904,8 @@
 
     @NativeMethods
     public interface Natives {
-        void onActivityStart(WebContents parentWebContent, WebContents webContents);
+        void onActivityStartForTesting( // IN-TEST
+                WebContents parentWebContent, WebContents webContents);
 
         void onBackToTab();
     }
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java
index 6ff9bb1..e3ea963b 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/media/DocumentPictureInPictureActivityTest.java
@@ -45,6 +45,7 @@
 import org.chromium.chrome.test.util.ChromeTabUtils;
 import org.chromium.content_public.browser.WebContents;
 import org.chromium.content_public.browser.test.util.JavaScriptUtils;
+import org.chromium.url.Origin;
 
 /** Tests for DocumentPictureInPictureActivity. */
 @RunWith(ChromeJUnit4ClassRunner.class)
@@ -65,7 +66,7 @@
 
     @Before
     public void setUp() {
-        mActivityTestRule.startOnBlankPage();
+        mActivityTestRule.startOnTestServerUrl("/chrome/test/data/android/simple.html");
         mTab = mActivityTestRule.getActivityTab();
         mParentWebContents = mTab.getWebContents();
 
@@ -81,6 +82,11 @@
         DocumentPictureInPictureActivity.setWebContentsForTesting(mWebContents);
         DocumentPictureInPictureActivity.setParentWebContentsForTesting(mParentWebContents);
         DocumentPictureInPictureActivity.setIgnoreSdkVersionForTesting(true);
+        ThreadUtils.runOnUiThreadBlocking(
+                () -> {
+                    DocumentPictureInPictureActivity.onActivityStartForTesting(
+                            mParentWebContents, mWebContents);
+                });
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.