CVE-2026-17690
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/android/pdf/internal/java/src/org/chromium/chrome/browser/pdf/PdfCoordinator.java |
modified | |
ifchrome/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.javachrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.javachrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtilsUnitTest.java
Patch
From 288c6286f479680c1d68a97b7d0ca734765b4799 Mon Sep 17 00:00:00 2001 From: Shu Yang <[email protected]> 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 <[email protected]> Reviewed-by: Min Qin <[email protected]> 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);
Original Bug Report
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.javachrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.javachrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfPage.javachrome/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.
- 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.
- Navigation Trigger: The assistant app launches Chrome with an explicit
ACTION_VIEWintent containing a crafted data URL:chrome-native://pdf/link?url=content%3A%2F%2Fcom.android.chrome.FileProvider%2Fpasswords%2FChrome%2520Passwords.csv - Intent Acceptance:
IntentHandler.isUrlUnsafe(inIntentHandler.javaline 1090) explicitly allows thechrome-native://pdf/URL to pass without restriction. - Page Construction:
TabImplevaluatesPdfUtils.isPdfNavigationwhich extracts the nestedcontent://URL. Since it is acontent://scheme, Chrome constructs aPdfPageand sets its filepath to the attacker-suppliedChromeFileProviderURI. - URI Assignment:
PdfCoordinatorassignsmUri = PdfUtils.getUriFromFilePath(mPdfFilePath). Even if the PDF rendering engine subsequently fails to load the file because it is not a valid PDF,mUriremains assigned to the targeted private CSV. - Assist Content Request: The assistant app programmatically triggers an assist session. The Android system calls
ChromeActivity.onProvideAssistContent, which callsPdfPage.requestAssistContentonce theEnterpriseInfocache resolves. - Permission Grant:
PdfCoordinator.requestAssistContentretrieves the assistant’s package name and callsmActivity.grantUriPermissionon the targetmUri. Android records a persistent READ grant for the malicious assistant package. - 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