CVE-2026-79055
Overview
Background
- Android PDF page URL
- Chrome on Android shows PDFs in a native page whose visible URL encodes the underlying document URI.
PdfUtils.decodePdfPageUrlrecovers that URI (for example acontent://URI), andPdfUtils.isDownloadedPdfdecides whether the page counts as a downloaded or local PDF. - Share intent with file URIs
- when
ShareParamscarriessetFileUris(...), the Android share sheet sends thosecontent://URIs to the app the user picks. Per the commit message, it also grants that app read permission on them. - Chrome's `FileProvider`
- Chrome’s own content provider (
content://org.chromium.chrome.FileProvider/...), which can expose private app files such as exported passwords (passwords/ChromePass.csvin the test) or netlogs. External apps must never be granted access to it arbitrarily. - `PdfUtils.isUriSafeForSharing`
- a check the patch calls (it is not defined in this diff). Per the commit message it allows only safe content URI paths, such as downloaded PDFs, and rejects Chrome’s private directories.
Root Cause Analysis
In ShareDelegateImpl.java, the share path decided whether to share a PDF as a file using only PdfUtils.isDownloadedPdf(visibleUrl.getSpec()). If that was true, it decoded the embedded URI with PdfUtils.decodePdfPageUrl and placed it directly into setFileUris(...) with MimeTypeUtils.PDF_MIME_TYPE. Nothing checked what that URI pointed to. The commit message explains that an external app could make Chrome navigate to a PDF page URL wrapping a nested content URI into Chrome’s private FileProvider. A later user share would then hand that private URI, with read permission, to whichever app the user chose. The violated rule is that Chrome should only grant other apps read access to content it intends to share, never to its private storage.
The fix requires both isDownloadedPdf and PdfUtils.isUriSafeForSharing(pdfUri, mContext) before attaching a file URI. getUrlToShare now returns the visible URL (a plain-URL share, with no file grant) instead of "" when the decoded URI is unsafe. The new unit test confirms that getFileUris() is null and the URL is the PDF page URL for content://org.chromium.chrome.FileProvider/passwords/ChromePass.csv.
PdfUtils.isUriSafeForSharing and falls back to a URL-only share without a permission grant.Attack Path
- Malicious app launches Chrome
an installed external app on the device makes Chrome navigate to a PDF page URL that encodes a nested content URI into Chrome’s private
FileProvider(per the commit message). This requires a malicious app on the device, not just web content. - Page is classed as a downloaded PDF
PdfUtils.isDownloadedPdfaccepts the visible URL, so the share path treats the decoded inner URI as the PDF file to share. - User shares the page
the user shares the tab (for example from the overflow menu,
ShareOrigin.OVERFLOW_MENUin the test). Before the fix,ShareParamscarried the privatecontent://URI as a file URI. - Target app receives read access if the user picks the attacker’s app in the share sheet, it receives the URI with read permission. It can then read the private file, such as exported passwords or netlogs named in the commit message, if such a file exists at that path.
Impact Assessment
FileProvider, which the commit message says could include password exports or netlogs. The preconditions are a malicious app on the device that can launch Chrome to a crafted PDF page URL, the user sharing that page, and the user choosing the malicious app in the share sheet. Whether a sensitive file actually exists at the targeted path depends on prior user actions (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/share/ShareDelegateImpl.java |
modified |
Files Changed
chrome/android/javatests/src/org/chromium/chrome/browser/share/ShareDelegateImplTest.javachrome/android/java/src/org/chromium/chrome/browser/share/ShareDelegateImpl.javachrome/android/junit/src/org/chromium/chrome/browser/share/ShareDelegateImplUnitTest.java
Audit Directions
- Decoded or nested URIs reaching intentslook for Android code that unwraps a URI from a Chrome URL (PDF pages, offline pages,
content://viewers, download UI) and passes it toIntentextras,ClipData, orsetFileUriswithout checking the authority and path against an allow-list. - Permission grants on share or openreview every place Chrome attaches
FLAG_GRANT_READ_URI_PERMISSIONor file URIs (share, “open with”, print, download-complete notifications) for whether the URI could point into Chrome’s ownFileProviderpaths. - External-intent-controlled navigationexamine how external apps can steer Chrome to internal or native-page URLs (PDF, distiller, offline) that later get trusted by privileged UI flows. Classify these by provenance, not just by URL shape.
Patch
From 6146f6b618ea43cec28efe58c40b456391b8944b Mon Sep 17 00:00:00 2001 From: Shu Yang <[email protected]> Date: Mon, 06 Jul 2026 16:01:21 -0700 Subject: [PATCH] Restrict shared PDF content URIs to safe paths An external app could trigger navigation to a nested content URI pointing to Chrome's private file provider. If the user subsequently shared this page, Chrome would include the private URI in the share intent with read permission, allowing potential exfiltration of sensitive files (e.g. passwords, netlogs) if the user selected the malicious app in the share sheet. This CL mitigates this by validating the PDF document URI before sharing it. We use PdfUtils.isUriSafeForSharing to allow only safe content URI paths (like downloaded PDFs) and block access to Chrome's private directories. Unsafe URIs will fallback to a plain URL share without granting read permissions. Bug: 497232609 Change-Id: I1b63a2d0a7254571f80e788a26e7fff3871bd07d Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8040877 Reviewed-by: Sirisha Kavuluru <[email protected]> Commit-Queue: Shu Yang <[email protected]> Cr-Commit-Position: refs/heads/main@{#1657542} --- diff --git a/chrome/android/java/src/org/chromium/chrome/browser/share/ShareDelegateImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/share/ShareDelegateImpl.java index a66190f..e6f25a2 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/share/ShareDelegateImpl.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/share/ShareDelegateImpl.java @@ -259,13 +259,17 @@ @ShareOrigin final int shareOrigin, final boolean shareDirectly) { ShareParams.Builder shareParamsBuilder = - new ShareParams.Builder(window, title, getUrlToShare(visibleUrl)); + new ShareParams.Builder(window, title, getUrlToShare(visibleUrl, mContext)); shareParamsBuilder.setOrigin(shareOrigin); - boolean isDownloadedPdf = PdfUtils.isDownloadedPdf(visibleUrl.getSpec()); - if (isDownloadedPdf) { + String decodedUrl = PdfUtils.decodePdfPageUrl(visibleUrl.getSpec()); + Uri pdfUri = decodedUrl != null ? Uri.parse(decodedUrl) : null; + boolean isDownloadedPdf = + PdfUtils.isDownloadedPdf(visibleUrl.getSpec()) + && PdfUtils.isUriSafeForSharing(pdfUri, mContext); + if (isDownloadedPdf && pdfUri != null) { ArrayList<Uri> fileToShare = new ArrayList<>(); - fileToShare.add(Uri.parse(PdfUtils.decodePdfPageUrl(visibleUrl.getSpec()))); + fileToShare.add(pdfUri); shareParamsBuilder .setFileUris(fileToShare) .setFileContentType(MimeTypeUtils.PDF_MIME_TYPE); @@ -285,8 +289,14 @@ } @VisibleForTesting - static String getUrlToShare(GURL visibleUrl) { - if (PdfUtils.isDownloadedPdf(visibleUrl.getSpec())) return ""; + static String getUrlToShare(GURL visibleUrl, @Nullable Context context) { + if (PdfUtils.isDownloadedPdf(visibleUrl.getSpec())) { + String decodedUrl = PdfUtils.decodePdfPageUrl(visibleUrl.getSpec()); + Uri pdfUri = decodedUrl != null ? Uri.parse(decodedUrl) : null; + if (context == null || PdfUtils.isUriSafeForSharing(pdfUri, context)) { + return ""; + } + } return visibleUrl.getSpec(); } diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/share/ShareDelegateImplTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/share/ShareDelegateImplTest.java index a86b0aa..8294157e 100644 --- a/chrome/android/javatests/src/org/chromium/chrome/browser/share/ShareDelegateImplTest.java +++ b/chrome/android/javatests/src/org/chromium/chrome/browser/share/ShareDelegateImplTest.java @@ -4,6 +4,9 @@ package org.chromium.chrome.browser.share; +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; import org.junit.Assert; @@ -22,9 +25,10 @@ @Test @SmallTest public void testGetUrlToShare() { - Assert.assertEquals("", ShareDelegateImpl.getUrlToShare(GURL.emptyGURL())); + Context context = ApplicationProvider.getApplicationContext(); + Assert.assertEquals("", ShareDelegateImpl.getUrlToShare(GURL.emptyGURL(), context)); final GURL httpsUrl = new GURL("https://blah.com"); - Assert.assertEquals(httpsUrl.getSpec(), ShareDelegateImpl.getUrlToShare(httpsUrl)); + Assert.assertEquals(httpsUrl.getSpec(), ShareDelegateImpl.getUrlToShare(httpsUrl, context)); } } diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/share/ShareDelegateImplUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/share/ShareDelegateImplUnitTest.java index 20dc99c3..06e6938a 100644 --- a/chrome/android/junit/src/org/chromium/chrome/browser/share/ShareDelegateImplUnitTest.java +++ b/chrome/android/junit/src/org/chromium/chrome/browser/share/ShareDelegateImplUnitTest.java @@ -17,6 +17,8 @@ import android.app.Activity; import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; import android.net.Uri; import android.os.Build; @@ -61,6 +63,8 @@ import org.chromium.components.browser_ui.bottomsheet.BottomSheetController; import org.chromium.components.browser_ui.share.ShareParams; import org.chromium.components.browser_ui.util.AutomotiveUtils; +import org.chromium.components.dom_distiller.core.DomDistillerUrlUtils; +import org.chromium.components.dom_distiller.core.DomDistillerUrlUtilsJni; import org.chromium.components.favicon.LargeIconBridgeJni; import org.chromium.components.feature_engagement.Tracker; import org.chromium.content_public.browser.RenderFrameHost; @@ -87,6 +91,7 @@ new OverrideContextWrapperTestRule(); @Mock private Context mContext; + @Mock private PackageManager mPackageManager; @Mock private RenderFrameHost mRenderFrameHost; @Mock private BottomSheetController mBottomSheetController; @Mock private ShareSheetDelegate mShareSheetController; @@ -105,6 +110,7 @@ @Mock SnackbarManager mSnackbarManager; @Mock private DataProtectionBridge.Natives mDataProtectionBridgeMock; + @Mock private DomDistillerUrlUtils.Natives mDomDistillerUrlUtilsJniMock; private final ArgumentCaptor<ShareParams> mShareParamsCaptor = ArgumentCaptor.forClass(ShareParams.class); @@ -166,6 +172,12 @@ TrackerFactory.setTrackerForTests(mTracker); Mockito.doReturn(new WeakReference<>(mActivity)).when(mWindowAndroid).getActivity(); DataProtectionBridge.setInstanceForTesting(mDataProtectionBridgeMock); + DomDistillerUrlUtilsJni.setInstanceForTesting(mDomDistillerUrlUtilsJniMock); + doAnswer(invocation -> new GURL((String) invocation.getArgument(0))) + .when(mDomDistillerUrlUtilsJniMock) + .getOriginalUrlFromDistillerUrl(anyString()); + doReturn(mPackageManager).when(mContext).getPackageManager(); + doReturn("org.chromium.chrome").when(mContext).getPackageName(); // TODO(crbug.com/406591712): Update to stubbing share methods when those are added. doAnswer(sShareIsAllowedByPolicy) @@ -735,4 +747,50 @@ "Page title should be set on ShareParams.", pdfTitle, params.getTitle()); Assert.assertEquals("URL should be empty on ShareParams.", "", params.getUrl()); } + + @Test + public void testShareUnsafePDf() { + final String pdfTitle = "unsafe.pdf"; + final String contentUri = + "content://org.chromium.chrome.FileProvider/passwords/ChromePass.csv"; + final String pdfUrl = PdfUtils.encodePdfPageUrl(contentUri); + doReturn(true).when(mTab).isNativePage(); + doReturn(new GURL(pdfUrl)).when(mTab).getUrl(); + doReturn(pdfTitle).when(mTab).getTitle(); + doReturn(mock(WindowAndroid.class)).when(mTab).getWindowAndroid(); + + // Setup mock package manager to identify this URI as coming from this app. + ProviderInfo providerInfo = new ProviderInfo(); + providerInfo.packageName = "org.chromium.chrome"; + doReturn(providerInfo) + .when(mPackageManager) + .resolveContentProvider("org.chromium.chrome.FileProvider", 0); + + createShareDelegate(false, mShareSheetController); + mShareDelegate.share(mTab, false, ShareOrigin.OVERFLOW_MENU); + verify(mShareSheetController) + .share( + mShareParamsCaptor.capture(), + any(), + any(), + any(), + any(), + any(), + any(), + any(), + any(), + anyInt(), + anyLong(), + anyBoolean(), + any(), + any(), + any(), + any()); + + ShareParams params = mShareParamsCaptor.getValue(); + // Should NOT be shared as file because it is unsafe. + Assert.assertNull("File URIs should be null for unsafe PDF.", params.getFileUris()); + // Should be shared as URL instead. + Assert.assertEquals("URL should be the visible PDF URL.", pdfUrl, params.getUrl()); + } }