Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in UI
DescriptionIncorrect security UI in UI
ComponentUI
Bug ClassLogic Error
Tracker513992796
Fix commit4698f5990d99 (chromium/src) +73/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Files Changed

  • chrome/android/java/res/layout/document_picture_in_picture_header_layout.xml
  • chrome/android/java/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderMediator.java
  • chrome/android/java/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderProperties.java
  • chrome/android/java/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderViewBinder.java
  • chrome/android/junit/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderMediatorUnitTest.java
From 4698f5990d99436a2ca11faaf46a075a7b040961 Mon Sep 17 00:00:00 2001
From: Phil Yan <[email protected]>
Date: Wed, 27 May 2026 16:18:46 -0700
Subject: [PATCH] [Doc-PiP] Implement conditional elision for PiP header URLs

This is a follow-up CL: in the original http://crrev.com/c/7862762, we
have addressed the reported URL spoofing issue for standard web URLs by
head-eliding the URLs.

However, this can allow spoofing of file:// and content:// URLs. To
prevent this, we implement conditional elision where local URLs are
tail-elided (so the scheme prefix remains visible) and standard web URLs
are head-elided, matching the desktop Doc-PiP elision behavior.

Bug: 513992796
Change-Id: Ida0e00d1e11448fd15c1675462fa7d45aac85d46
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7872096
Commit-Queue: Phil Yan <[email protected]>
Reviewed-by: Frank Liberato <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1637316}
---

diff --git a/chrome/android/java/res/layout/document_picture_in_picture_header_layout.xml b/chrome/android/java/res/layout/document_picture_in_picture_header_layout.xml
index 05549fc..c1309b7 100644
--- a/chrome/android/java/res/layout/document_picture_in_picture_header_layout.xml
+++ b/chrome/android/java/res/layout/document_picture_in_picture_header_layout.xml
@@ -28,7 +28,10 @@
             "@dimen/document_picture_in_picture_header_component_size"
         android:scaleType="center" />
 
-    <!-- Ellipsize start (head elision) is consistent with desktop (ELIDE_HEAD) to prevent origin spoofing. -->
+    <!-- Ellipsize START is set as the default here for layout editor previewing and
+         initial inflation. The behavior is overridden programmatically:
+         - START (head) elision for web URLs to prevent origin spoofing.
+         - END (tail) elision for file URLs to prevent local path spoofing. -->
     <TextView
         android:id="@+id/document_picture_in_picture_header_url_bar"
         android:background="@null"
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderMediator.java b/chrome/android/java/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderMediator.java
index 729db267..044f0e2c 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderMediator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderMediator.java
@@ -7,6 +7,7 @@
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.graphics.Rect;
+import android.text.TextUtils;
 import android.view.View;
 
 import androidx.annotation.ColorInt;
@@ -113,9 +114,15 @@
         onAppHeaderStateChanged(mDesktopWindowStateManager.getAppHeaderState());
 
         updateSecurityIcon();
+        GURL visibleUrl = mOpenerWebContents.getVisibleUrl();
+        mModel.set(DocumentPictureInPictureHeaderProperties.URL_STRING, getUrlString(visibleUrl));
+        // To prevent spoofing, local URLs are tail-elided (keeping the scheme prefix
+        // visible) and standard web URLs are head-elided, matching desktop elision behavior.
         mModel.set(
-                DocumentPictureInPictureHeaderProperties.URL_STRING,
-                getUrlString(mOpenerWebContents.getVisibleUrl()));
+                DocumentPictureInPictureHeaderProperties.URL_ELLIPSIZE_BEHAVIOR,
+                isLocalFileOrContentScheme(visibleUrl.getScheme())
+                        ? TextUtils.TruncateAt.END
+                        : TextUtils.TruncateAt.START);
 
         mThemeColorProvider.addThemeColorObserver(this);
         mThemeColorProvider.addTintObserver(this);
@@ -269,6 +276,11 @@
                 DocumentPictureInPictureHeaderProperties.NON_DRAGGABLE_AREAS, mNonDraggableAreas);
     }
 
+    private boolean isLocalFileOrContentScheme(@Nullable String scheme) {
+        return UrlConstants.FILE_SCHEME.equals(scheme)
+                || UrlConstants.CONTENT_SCHEME.equals(scheme);
+    }
+
     private String getUrlString(GURL url) {
         if (url.getScheme().equals(UrlConstants.FILE_SCHEME)) {
             // File scheme URLs do not have a host, so we use the path instead.
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderProperties.java b/chrome/android/java/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderProperties.java
index 5fdeeed..2c61a857 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderProperties.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderProperties.java
@@ -6,6 +6,7 @@
 
 import android.content.res.ColorStateList;
 import android.graphics.Rect;
+import android.text.TextUtils;
 import android.view.View;
 
 import androidx.core.graphics.Insets;
@@ -47,6 +48,8 @@
             ON_SECURITY_ICON_CLICK_LISTENER = new WritableObjectPropertyKey<>();
     public static final WritableObjectPropertyKey<String> URL_STRING =
             new WritableObjectPropertyKey<>();
+    public static final WritableObjectPropertyKey<TextUtils.TruncateAt> URL_ELLIPSIZE_BEHAVIOR =
+            new WritableObjectPropertyKey<>();
     public static final WritableIntPropertyKey BRANDED_COLOR_SCHEME = new WritableIntPropertyKey();
 
     public static final PropertyKey[] ALL_KEYS = {
@@ -63,6 +66,7 @@
         SECURITY_ICON_CONTENT_DESCRIPTION_RES_ID,
         ON_SECURITY_ICON_CLICK_LISTENER,
         URL_STRING,
+        URL_ELLIPSIZE_BEHAVIOR,
         BRANDED_COLOR_SCHEME
     };
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderViewBinder.java b/chrome/android/java/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderViewBinder.java
index 189b5c1..1bb6cff 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderViewBinder.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderViewBinder.java
@@ -5,6 +5,7 @@
 package org.chromium.chrome.browser.media.document_picture_in_picture_header;
 
 import android.content.res.ColorStateList;
+import android.text.TextUtils;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageView;
@@ -94,6 +95,10 @@
                                             .ON_SECURITY_ICON_CLICK_LISTENER));
         } else if (key == DocumentPictureInPictureHeaderProperties.URL_STRING) {
             updateUrl(view, model.get(DocumentPictureInPictureHeaderProperties.URL_STRING));
+        } else if (key == DocumentPictureInPictureHeaderProperties.URL_ELLIPSIZE_BEHAVIOR) {
+            updateUrlEllipsizeBehavior(
+                    view,
+                    model.get(DocumentPictureInPictureHeaderProperties.URL_ELLIPSIZE_BEHAVIOR));
         } else if (key == DocumentPictureInPictureHeaderProperties.BRANDED_COLOR_SCHEME) {
             updateBrandedColorScheme(
                     view, model.get(DocumentPictureInPictureHeaderProperties.BRANDED_COLOR_SCHEME));
@@ -133,4 +138,9 @@
         urlBar.setText(urlHost);
         urlBar.setTooltipText(urlHost);
     }
+
+    private static void updateUrlEllipsizeBehavior(View view, TextUtils.TruncateAt behavior) {
+        TextView urlBar = view.findViewById(R.id.document_picture_in_picture_header_url_bar);
+        urlBar.setEllipsize(behavior);
+    }
 }
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderMediatorUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderMediatorUnitTest.java
index c6300b1..6dfe4df 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderMediatorUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderMediatorUnitTest.java
@@ -21,6 +21,7 @@
 import android.content.res.ColorStateList;
 import android.graphics.Color;
 import android.graphics.Rect;
+import android.text.TextUtils;
 import android.view.View;
 import android.widget.FrameLayout;
 
@@ -82,6 +83,7 @@
             BrandedColorScheme.LIGHT_BRANDED_THEME;
     private static final GURL HTTPS_URL = JUnitTestGURLs.EXAMPLE_URL;
     private static final GURL LOCAL_FILE_URL = new GURL("file:///android_asset/index.html");
+    private static final GURL CONTENT_URL = new GURL("content://media/external/images/media/1");
 
     private Context mContext;
     private PropertyModel mModel;
@@ -105,7 +107,11 @@
                 .thenAnswer(
                         invocation -> {
                             GURL url = invocation.getArgument(0);
-                            return url.getHost();
+                            String scheme = url.getScheme();
+                            if (scheme.equals("http") || scheme.equals("https")) {
+                                return url.getHost();
+                            }
+                            return url.getSpec();
                         });
         mOpenerWebContents =
                 Mockito.mock(
@@ -170,6 +176,9 @@
         assertEquals(
                 HTTPS_URL.getHost(),
                 mModel.get(DocumentPictureInPictureHeaderProperties.URL_STRING));
+        assertEquals(
+                TextUtils.TruncateAt.START,
+                mModel.get(DocumentPictureInPictureHeaderProperties.URL_ELLIPSIZE_BEHAVIOR));
     }
 
     @Test
@@ -466,6 +475,22 @@
         assertEquals(
                 LOCAL_FILE_URL.getPath(),
                 mModel.get(DocumentPictureInPictureHeaderProperties.URL_STRING));
+        assertEquals(
+                TextUtils.TruncateAt.END,
+                mModel.get(DocumentPictureInPictureHeaderProperties.URL_ELLIPSIZE_BEHAVIOR));
+    }
+
+    @Test
+    @SmallTest
+    public void testContentUrl() {
+        createMediator(/* isBackToTabShown= */ true, CONTENT_URL);
+
+        assertEquals(
Loading diff…

Original Bug Report

reported by [email protected]

Potential origin spoofing in Android Document Picture-in-Picture header via URL truncation

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: The browser-drawn header for Document Picture-in-Picture on Android is vulnerable to origin spoofing because it does not ensure the visibility of the registrable domain. An attacker can craft a long subdomain and specify a narrow window width to hide their actual domain while leaving a trusted prefix visible. This behavior deviates from the Desktop implementation, which correctly elides the start of the origin.

Affected files:

  • chrome/android/java/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderMediator.java
  • chrome/android/java/src/org/chromium/chrome/browser/media/document_picture_in_picture_header/DocumentPictureInPictureHeaderViewBinder.java
  • chrome/android/java/res/layout/document_picture_in_picture_header_layout.xml

Estimated timestamp from git blame: Unknown (Google3 checkout)

Summary

A potential origin spoofing vulnerability exists in the Android implementation of Document Picture-in-Picture (PiP). The browser-managed header, which is responsible for origin attribution, uses improper URL formatting and truncation logic. By using a long subdomain (e.g., https://accounts.google.com.malicious-site.com) and controlling the PiP window width, an attacker can potentially hide the registrable domain, displaying only a trusted prefix in the header.

Technical Details

The vulnerability stems from several factors in the Android UI implementation for Document PiP:

  1. Improper Host Extraction: In DocumentPictureInPictureHeaderMediator.java, the method getUrlString(GURL url) (line 282) returns the raw host using url.getHost(). It fails to use UrlFormatter.formatUrlForSecurityDisplay(), which is used in other Chrome surfaces to safely format URLs and protect against homograph attacks.
  2. Insecure Truncation (Ellipsizing): The header’s URL bar is a standard Android TextView defined in document_picture_in_picture_header_layout.xml (line 31). It is configured with android:singleLine="true" and android:ellipsize="end" (line 44). This causes the Android system to truncate characters from the right side when the text exceeds the available width.
  3. Attacker-Controlled Viewport: An attacker can control the width of the PiP window via the documentPictureInPicture.requestWindow({width}) API. By choosing a specific width (e.g., 220dp), the attacker can ensure that only a trusted portion of their long host string is displayed in the space available between the browser’s security icon and the ‘back to tab’ button.

This behavior lacks parity with the Desktop implementation (chrome/browser/ui/views/frame/picture_in_picture_browser_frame_view.cc, line 585), which explicitly uses gfx::ELIDE_HEAD for HTTPS hosts to ensure the registrable domain (the end of the string) is always visible.

Potential Reproduction Steps

Note: These steps are based on code analysis and have not been verified with a functional proof-of-concept.

  1. Enable the Document Picture-in-Picture feature on an Android 14+ device (e.g., via chrome://flags/#enable-android-document-picture-in-picture).
  2. Navigate to a site with a long subdomain prefix designed to spoof a trusted origin: https://accounts.google.com.secure.login.attacker.com.
  3. Trigger a Document PiP window with a narrow width:
    const w = await documentPictureInPicture.requestWindow({width: 220, height: 300});
    
  4. Observe the browser-drawn header. Due to ellipsize="end", the header may show ‘accounts.google.com…’ while the actual domain ‘attacker.com’ is hidden by truncation.

Suggested Fix

  1. Modify DocumentPictureInPictureHeaderMediator.java to use UrlFormatter.formatUrlForSecurityDisplay() when retrieving the host string for display.
  2. Update document_picture_in_picture_header_layout.xml to use android:ellipsize="start" (parity with ELIDE_HEAD) for the URL bar TextView. This ensures that the most critical part of the origin—the registrable domain—remains visible even when the host is truncated.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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