Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in PDF
DescriptionInsufficient validation of untrusted input in PDF
ComponentPDF
Bug ClassLogic Error
Tracker517129282
Fix commit288c6286f479 (chromium/src) +269/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
chrome/browser/ui/android/pdf/internal/java/src/org/chromium/chrome/browser/pdf/PdfCoordinator.java
modified
if
chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java
modified

Files Changed

  • chrome/browser/ui/android/pdf/internal/java/src/org/chromium/chrome/browser/pdf/PdfCoordinator.java
  • chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java
  • chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtilsUnitTest.java
From 288c6286f479680c1d68a97b7d0ca734765b4799 Mon Sep 17 00:00:00 2001
From: Shu Yang <shuyng@google.com>
Date: Sat, 06 Jun 2026 00:01:18 -0700
Subject: [PATCH] Restrict shared content URIs in PdfCoordinator to safe paths

A default digital assistant can trigger navigation to a nested
content URI pointing to Chrome's private file provider. During
the assist session, PdfCoordinator would grant the assistant
persistent read permission to this unvalidated URI, allowing
potential exfiltration of sensitive files like passwords or netlogs.

This CL mitigates this by validating the PDF document URI before
granting read permissions or sharing it with the assistant.
We introduce PdfUtils.isUriSafeForSharing to whitelist only
safe content URI paths:
- For ChromeFileProvider: "pdfs" (cache) and "downloads" (public).
- For DownloadFileProvider: "download", "download_external",
  and "external_volume" (SD card downloads).
- PdfContentProvider is allowed.
- Non-Chrome providers and non-content URIs are allowed.
- All other Chrome internal providers are blocked by default.

Bug: 517129282
Change-Id: I37bc4ca0538a0dfeea273631e020594c751c1491
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7887132
Commit-Queue: Shu Yang <shuyng@google.com>
Reviewed-by: Min Qin <qinmin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1642758}
---

diff --git a/chrome/browser/ui/android/pdf/internal/java/src/org/chromium/chrome/browser/pdf/PdfCoordinator.java b/chrome/browser/ui/android/pdf/internal/java/src/org/chromium/chrome/browser/pdf/PdfCoordinator.java
index 04fc219..00365cf 100644
--- a/chrome/browser/ui/android/pdf/internal/java/src/org/chromium/chrome/browser/pdf/PdfCoordinator.java
+++ b/chrome/browser/ui/android/pdf/internal/java/src/org/chromium/chrome/browser/pdf/PdfCoordinator.java
@@ -653,6 +653,11 @@
             return null;
         }
 
+        if (!PdfUtils.isUriSafeForSharing(mUri, mActivity)) {
+            Log.e(TAG, "Blocked getFileUri for unsafe URI: " + mUri);
+            return null;
+        }
+
         if (targetPackage == null) {
             targetPackage = PackageUtils.getDefaultAssistantPackageName(mActivity);
             PdfUtils.recordGetAssistantPackageResult(targetPackage != null);
@@ -671,6 +676,12 @@
         if (mUri == null) {
             return null;
         }
+
+        if (!PdfUtils.isUriSafeForSharing(mUri, mActivity)) {
+            Log.e(TAG, "Blocked requestAssistContent for unsafe URI: " + mUri);
+            return null;
+        }
+
         String structuredData;
         try {
             structuredData =
diff --git a/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java b/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java
index 10ecd030..0756d2f 100644
--- a/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java
+++ b/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java
@@ -6,6 +6,8 @@
 
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
 import android.net.Uri;
 import android.os.Build;
 import android.os.ext.SdkExtensions;
@@ -432,4 +434,84 @@
     public static void recordIsUriNull(boolean isNull) {
         RecordHistogram.recordBooleanHistogram("Android.Pdf.UriIsNull", isNull);
     }
+
+    /**
+     * Checks if the given URI is valid and safe for sharing with external applications.
+     * Specifically, if the URI belongs to one of Chrome's internal content providers, we restrict
+     * sharing to only designated safe paths (like downloaded PDFs).
+     *
+     * @param uri The URI to validate.
+     * @param context The context to retrieve package and provider info.
+     * @return True if the URI is safe for sharing, false otherwise.
+     */
+    public static boolean isUriSafeForSharing(@Nullable Uri uri, Context context) {
+        if (uri == null) {
+            return false;
+        }
+
+        String scheme = uri.getScheme();
+        // Non-content URIs (like file:// or https://) are safe because they either rely on the OS
+        // sandbox to restrict access (file://) or do not expose local files (https://).
+        if (!UrlConstants.CONTENT_SCHEME.equals(scheme)) {
+            return true;
+        }
+
+        String authority = uri.getAuthority();
+        if (TextUtils.isEmpty(authority)) {
+            return false;
+        }
+
+        PackageManager pm = context.getPackageManager();
+        ProviderInfo providerInfo = pm.resolveContentProvider(authority, 0);
+        // If the provider cannot be resolved, it is either not registered or belongs to a
+        // third-party app that Chrome cannot query due to Android package visibility restrictions.
+        // In either case, it is not Chrome's provider, so it is safe to allow.
+        if (providerInfo == null) {
+            return true;
+        }
+
+        String myPackageName = context.getPackageName();
+        // We only restrict URIs pointing to Chrome's own providers. Third-party providers
+        // are responsible for their own security.
+        if (!myPackageName.equals(providerInfo.packageName)) {
+            return true;
+        }
+
+        // Chrome's main FileProvider (uses file_paths.xml). We only allow sharing from the
+        // temporary PDF cache ("pdfs") and the public downloads folder ("downloads").
+        // Sensitive directories like "passwords" or "cache" (net-export) are blocked.
+        if (authority.endsWith(".FileProvider")) {
+            List<String> pathSegments = uri.getPathSegments();
+            if (pathSegments == null || pathSegments.isEmpty()) {
+                return false;
+            }
+            String firstSegment = pathSegments.get(0);
+            return "pdfs".equals(firstSegment) || "downloads".equals(firstSegment);
+        }
+
+        // Chrome's custom DownloadFileProvider (used for SD cards).
+        // It programmatically restricts access to download directories only. We whitelist
+        // its valid path segments: "download" (primary storage fallback), "download_external"
+        // (legacy SD card), and "external_volume" (Android R+ SD card).
+        if (authority.endsWith(".DownloadFileProvider")) {
+            List<String> pathSegments = uri.getPathSegments();
+            if (pathSegments == null || pathSegments.isEmpty()) {
+                return false;
+            }
+            String firstSegment = pathSegments.get(0);
+            return "download".equals(firstSegment)
+                    || "download_external".equals(firstSegment)
+                    || "external_volume".equals(firstSegment);
+        }
+
+        // PdfContentProvider is a dedicated, unexported provider designed solely for serving
+        // PDF content safely. All its paths are safe.
+        if (authority.endsWith(".PdfContentProvider")) {
+            return true;
+        }
+
+        // Fallback: block any other internal Chrome providers to prevent accidental exposure of
+        // sensitive data (e.g. ChromeBrowserProvider).
+        return false;
+    }
 }
diff --git a/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtilsUnitTest.java b/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtilsUnitTest.java
index dd4763b..2a6098ec 100644
--- a/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtilsUnitTest.java
+++ b/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtilsUnitTest.java
@@ -12,6 +12,8 @@
 
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
 import android.net.Uri;
 import android.os.Build;
 import android.os.ext.SdkExtensions;
@@ -47,6 +49,7 @@
     @Mock private NativePage mNativePage;
     @Mock private Context mContext;
     @Mock private ContentResolver mContentResolver;
+    @Mock private PackageManager mPackageManager;
     private String mPdfPageUrl;
     private String mPdfPageBlobUrl;
 
@@ -286,6 +289,179 @@
         Assert.assertTrue("The encoded url should not exist", TextUtils.isEmpty(encodedUrl));
     }
 
+    @Test
+    public void testIsUriSafeForSharing_NullUri() {
+        Assert.assertFalse(PdfUtils.isUriSafeForSharing(null, mContext));
+    }
+
+    @Test
+    public void testIsUriSafeForSharing_NonContentScheme() {
+        Uri fileUri = Uri.parse("file:///sdcard/Downloads/sample.pdf");
+        Assert.assertTrue(PdfUtils.isUriSafeForSharing(fileUri, mContext));
+
+        Uri httpsUri = Uri.parse("https://example.com/sample.pdf");
+        Assert.assertTrue(PdfUtils.isUriSafeForSharing(httpsUri, mContext));
+    }
+
+    @Test
+    public void testIsUriSafeForSharing_UnresolvedProvider() {
+        Uri contentUri = Uri.parse("content://unknown.provider/sample.pdf");
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential unvalidated URI grant in PdfCoordinator allows assistant app to read private files

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: A malicious app set as the default digital assistant can potentially trigger navigation in Chrome to a crafted inline PDF URL targeting Chrome’s private ChromeFileProvider roots. During an assist session, PdfCoordinator grants the assistant app persistent read permission to this unvalidated URI. This could allow the assistant to exfiltrate sensitive files, such as exported password CSVs and NetLog session data.

Affected files:

  • chrome/browser/ui/android/pdf/internal/java/src/org/chromium/chrome/browser/pdf/PdfCoordinator.java
  • chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java
  • chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfPage.java
  • chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java

Estimated timestamp from git blame: 2025-05-15

Root Cause Analysis

In Chrome for Android, the inline PDF viewer uses PdfCoordinator to manage PDF documents. When an assist session is initiated (e.g., when the digital assistant is programmatically or manually triggered), Chrome overrides onProvideAssistContent and delegates the request to the active page. For PdfPage, this invokes PdfCoordinator.requestAssistContent:

public @Nullable String requestAssistContent(String filename, boolean isWorkProfile) {
    if (mUri == null) return null;
    ...
    var assistantPackageName = PackageUtils.getDefaultAssistantPackageName(mActivity);
    if (assistantPackageName != null) {
        mActivity.grantUriPermission(
                assistantPackageName, mUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
    }
    return structuredData;
}

There is a similar flow in PdfCoordinator.getFileUri:

public @Nullable Uri getFileUri(boolean isWorkProfile, @Nullable String targetPackage) {
    if (mUri == null) return null;
    ...
    if (targetPackage != null) {
        mActivity.grantUriPermission(
                targetPackage, mUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
    }
    ...
}

This creates a vulnerability because mUri is populated from an externally supplied URL without validating its scheme, authority, or path. PdfUtils.getUriFromFilePath parses the path into a URI and returns any content:// or file:// scheme verbatim:

public static @Nullable Uri getUriFromFilePath(String pdfFilePath) {
    Uri uri = Uri.parse(pdfFilePath);
    String scheme = uri.getScheme();
    if (UrlConstants.CONTENT_SCHEME.equals(scheme) || UrlConstants.FILE_SCHEME.equals(scheme)) {
        return uri;
    }
    ...
}

Because Chrome defines ChromeFileProvider with grantUriPermissions="true" in its AndroidManifest.xml, the Android OS allows Chrome to issue self-grants for its own files. By navigating Chrome to a crafted chrome-native://pdf/ URL targeting Chrome’s own FileProvider roots (configured in file_paths.xml, exposing directories such as passwords/ and net-export/), a malicious assistant app can coerce Chrome into granting it access to highly sensitive, browser-private data.


Potential Trigger Path

Note: These are potential, theoretical steps constructed from static code analysis; our automated tools do not have the capability to run code or verify runtime behavior.

  1. Preconditions: The user is on Android 12+ (with SDK extension >= 13) or Android 15+. The user has configured the attacker’s app as their default Digital Assistant.
  2. Navigation Trigger: The assistant app launches Chrome with an explicit ACTION_VIEW intent containing a crafted data URL: chrome-native://pdf/link?url=content%3A%2F%2Fcom.android.chrome.FileProvider%2Fpasswords%2FChrome%2520Passwords.csv
  3. Intent Acceptance: IntentHandler.isUrlUnsafe (in IntentHandler.java line 1090) explicitly allows the chrome-native://pdf/ URL to pass without restriction.
  4. Page Construction: TabImpl evaluates PdfUtils.isPdfNavigation which extracts the nested content:// URL. Since it is a content:// scheme, Chrome constructs a PdfPage and sets its filepath to the attacker-supplied ChromeFileProvider URI.
  5. URI Assignment: PdfCoordinator assigns mUri = PdfUtils.getUriFromFilePath(mPdfFilePath). Even if the PDF rendering engine subsequently fails to load the file because it is not a valid PDF, mUri remains assigned to the targeted private CSV.
  6. Assist Content Request: The assistant app programmatically triggers an assist session. The Android system calls ChromeActivity.onProvideAssistContent, which calls PdfPage.requestAssistContent once the EnterpriseInfo cache resolves.
  7. Permission Grant: PdfCoordinator.requestAssistContent retrieves the assistant’s package name and calls mActivity.grantUriPermission on the target mUri. Android records a persistent READ grant for the malicious assistant package.
  8. Exfiltration: The assistant app reads the URI via getContentResolver().openInputStream(uri), exfiltrating the private passwords.

Suggested Fix

Validate the authority of mUri before calling grantUriPermission. Specifically, PdfCoordinator should verify that the URI to be shared does not point to Chrome’s own internal authorities (e.g., <manifest_package>.FileProvider) unless explicitly authorized for sharing. Alternatively, PdfUtils should reject any local content:// or file:// URIs that resolve to Chrome’s private directory structures during URL parsing.

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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.

Raised in root component due to access or custom field issues on 1279895

View on issue tracker