Chrome · PDF
CVE-2026-106328
Authorization Error in PDF
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
TESTchrome/browser/ui/startup/url_util_unittest.cc |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.javachrome/android/junit/src/org/chromium/chrome/browser/IntentHandlerRobolectricTest.javachrome/android/java/src/org/chromium/chrome/browser/ExternalIntentUrlChecker.javachrome/android/junit/src/org/chromium/chrome/browser/handoff/HandoffControllerUnitTest.javachrome/android/junit/src/org/chromium/chrome/browser/ExternalIntentUrlCheckerTest.javachrome/browser/ui/startup/url_util_unittest.cc
Patch
From b868931d1055af3089b25c61f4742a317510dc44 Mon Sep 17 00:00:00 2001 From: Shu Yang <[email protected]> Date: Fri, 11 Sep 2026 13:05:03 -0700 Subject: [PATCH] Prevent external intent validation bypass for local PDF and content URIs This CL addresses an external intent validation bypass that could allow a local malicious app to force Chrome to read and share its own protected files via a Confused Deputy attack. Part 1: Validate original target URLs Previously, IntentHandler automatically encoded incoming content URIs with PDF MIME types into internal chrome-native:// URLs prior to URL validation. This CL separates target URL extraction from internal encoding by introducing getTargetUrlFromIntent(). This allows IntentHandler to validate the original target URL before any internal transformations. Consequently, the allowlist exception for UrlConstants.PDF_URL in ExternalIntentUrlChecker is removed. Part 2: Block external intents targeting Chrome's Content Providers To further prevent malicious apps from instructing Chrome to open internal Chrome-owned content URIs, ExternalIntentUrlChecker is updated to explicitly reject intents containing a content:// scheme if the authority belongs to Chrome (starts with Chrome's package name). This prevents external intents from weaponizing any of Chrome's internal Content Providers. Bug: 518091868 Change-Id: I3bcea41255f8f3bd406413bb1eddf8c2b5eecdc2 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8383017 Commit-Queue: Shu Yang <[email protected]> Reviewed-by: Wenyu Fu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1696532} --- diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ExternalIntentUrlChecker.java b/chrome/android/java/src/org/chromium/chrome/browser/ExternalIntentUrlChecker.java index cc5312f1..c1773e30 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/ExternalIntentUrlChecker.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/ExternalIntentUrlChecker.java @@ -7,6 +7,7 @@ import org.jni_zero.JNINamespace; import org.jni_zero.NativeMethods; +import org.chromium.base.ContextUtils; import org.chromium.build.annotations.NullMarked; import org.chromium.build.annotations.Nullable; import org.chromium.components.embedder_support.util.UrlConstants; @@ -48,6 +49,19 @@ return true; } + // Block external intents that target Chrome's own content providers (e.g., FileProvider). + // Untrusted apps should not be able to force Chrome to open its own protected files, + // which could lead to Confused Deputy vulnerabilities. + if (UrlConstants.CONTENT_SCHEME.equals(scheme)) { + String host = url.getHost(); + if (host != null && !host.isEmpty()) { + String packageName = ContextUtils.getApplicationContext().getPackageName(); + if (host.equals(packageName) || host.startsWith(packageName + ".")) { + return true; + } + } + } + String urlString = url.getSpec(); if (!ExternalIntentUrlCheckerJni.get().validateUrl(url)) { @@ -56,8 +70,7 @@ if (urlString.equals(ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL) || urlString.equals(ContentUrlConstants.ABOUT_BLANK_URL) || urlString.equals(UrlConstants.CHROME_DINO_URL) - || urlString.startsWith(UrlConstants.CHROME_EXTENSIONS_URL) - || urlString.startsWith(UrlConstants.PDF_URL)) { + || urlString.startsWith(UrlConstants.CHROME_EXTENSIONS_URL)) { return false; } return true; diff --git a/chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java b/chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java index 9b25dcf..77057dc 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/IntentHandler.java @@ -1072,7 +1072,7 @@ return tabIdsToUrls.isEmpty(); } else { return shouldIgnoreIntentUrl( - intent, context, getUrlFromIntent(intent), isCustomTab); + intent, context, getTargetUrlFromIntent(intent), isCustomTab); } } catch (Throwable t) { return true; @@ -1275,6 +1275,20 @@ } /** + * Extracts the target URL from the intent, unwrapping googlechrome:// if present. + * + * @param intent Intent to examine. + * @return Target URL from the Intent, or null if a valid URL couldn't be found. + */ + private static @Nullable String getTargetUrlFromIntent(@Nullable Intent intent) { + String url = extractUrlFromIntent(intent); + if (isGoogleChromeScheme(url)) { + url = ExternalNavigationHandler.getUrlFromSelfSchemeUrl(GOOGLECHROME_SCHEME, url); + } + return url; + } + + /** * Retrieve the URL from the Intent, which may be in multiple locations. If the URL is * googlechrome:// scheme, parse the actual navigation URL. * @@ -1282,10 +1296,7 @@ * @return URL from the Intent, or null if a valid URL couldn't be found. */ public static @Nullable String getUrlFromIntent(@Nullable Intent intent) { - String url = extractUrlFromIntent(intent); - if (isGoogleChromeScheme(url)) { - url = ExternalNavigationHandler.getUrlFromSelfSchemeUrl(GOOGLECHROME_SCHEME, url); - } + String url = getTargetUrlFromIntent(intent); // To display a PDF in Chrome, the content URI must be encoded. String encodedPdfUrl = PdfUtils.getEncodedContentUri(url, ContextUtils.getApplicationContext()); diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/ExternalIntentUrlCheckerTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/ExternalIntentUrlCheckerTest.java index ede4dcbc..a5c92521 100644 --- a/chrome/android/junit/src/org/chromium/chrome/browser/ExternalIntentUrlCheckerTest.java +++ b/chrome/android/junit/src/org/chromium/chrome/browser/ExternalIntentUrlCheckerTest.java @@ -17,6 +17,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import org.chromium.base.ContextUtils; import org.chromium.base.test.BaseRobolectricTestRunner; import org.chromium.url.GURL; import org.chromium.url.JUnitTestGURLs; @@ -92,9 +93,36 @@ assertFalse( ExternalIntentUrlChecker.isUnsafeExternalIntentUrl( new GURL("chrome://extensions/?id=abcdef"))); - assertFalse( + assertTrue( ExternalIntentUrlChecker.isUnsafeExternalIntentUrl( new GURL("chrome-native://pdf/viewer.html"))); + assertTrue( + ExternalIntentUrlChecker.isUnsafeExternalIntentUrl( + new GURL("chrome-native://pdf/link?url=content://media/external/file/1"))); + } + + @Test + public void testIsUnsafeExternalIntentUrl_BlocksChromeFileProvider() { + doReturn(true).when(mNativeMock).validateUrl(any()); + + String packageName = ContextUtils.getApplicationContext().getPackageName(); + assertTrue( + ExternalIntentUrlChecker.isUnsafeExternalIntentUrl( + new GURL("content://" + packageName + ".FileProvider/document.pdf"))); + assertTrue( + ExternalIntentUrlChecker.isUnsafeExternalIntentUrl( + new GURL( + "content://userinfo@" + + packageName + + ".FileProvider/document.pdf"))); + assertTrue( + ExternalIntentUrlChecker.isUnsafeExternalIntentUrl( + new GURL("content://" + packageName + "/document.pdf"))); + + // Should not block legitimate external providers that start with Chrome's package name + assertFalse( + ExternalIntentUrlChecker.isUnsafeExternalIntentUrl( + new GURL("content://" + packageName + "cast.FileProvider/document.pdf"))); } @Test diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/IntentHandlerRobolectricTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/IntentHandlerRobolectricTest.java index 688f62c0..cdb2502 100644 --- a/chrome/android/junit/src/org/chromium/chrome/browser/IntentHandlerRobolectricTest.java +++ b/chrome/android/junit/src/org/chromium/chrome/browser/IntentHandlerRobolectricTest.java @@ -754,6 +754,10 @@ trustedIntent.setData(Uri.parse("chrome-native://newtab")); Assert.assertFalse(IntentHandler.shouldIgnoreIntent(trustedIntent, null)); + + trustedIntent.setData( + Uri.parse("chrome-native://pdf/link?url=content%3A%2F%2Ftest.provider%2Ftest.pdf")); + Assert.assertFalse(IntentHandler.shouldIgnoreIntent(trustedIntent, null)); } @Test @@ -769,6 +773,10 @@ untrustedIntent.setData(Uri.parse("chrome-native://newtab")); Assert.assertTrue(IntentHandler.shouldIgnoreIntent(untrustedIntent, null)); + untrustedIntent.setData( + Uri.parse("chrome-native://pdf/link?url=content%3A%2F%2Ftest.provider%2Ftest.pdf")); + Assert.assertTrue(IntentHandler.shouldIgnoreIntent(untrustedIntent, null)); + untrustedIntent.addCategory("com.fake.category"); Assert.assertTrue(IntentHandler.shouldIgnoreIntent(untrustedIntent, null)); } @@ -776,6 +784,27 @@ @Test @SmallTest @Feature({"Android-AppBase"}) + public void testContentPdfUrlAllowedForExternalSources() { + Mockito.doReturn(true).when(mExternalIntentUrlCheckerNativeMock).validateUrl(any()); + Intent externalIntent = new Intent(Intent.ACTION_VIEW); + externalIntent.setData(Uri.parse("content://test.provider/document.pdf")); + Assert.assertFalse(IntentHandler.shouldIgnoreIntent(externalIntent, null)); + } + + @Test + @SmallTest + @Feature({"Android-AppBase"}) + public void testChromeFileProviderBlockedForUntrustedIntent() { + Intent untrustedIntent = new Intent(Intent.ACTION_VIEW); + String packageName = ContextUtils.getApplicationContext().getPackageName(); + untrustedIntent.setData( + Uri.parse("content://" + packageName + ".FileProvider/document.pdf")); + Assert.assertTrue(IntentHandler.shouldIgnoreIntent(untrustedIntent, null)); + } + + @Test + @SmallTest + @Feature({"Android-AppBase"}) public void testChromeInternalUrlsAllowedForWhitelistedUrls() { Intent untrustedIntent = new Intent(Intent.ACTION_VIEW); Assert.assertFalse(IntentHandler.wasIntentSenderChrome(untrustedIntent)); diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/handoff/HandoffControllerUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/handoff/HandoffControllerUnitTest.java index 1d4a898..31d269ec 100644 --- a/chrome/android/junit/src/org/chromium/chrome/browser/handoff/HandoffControllerUnitTest.java +++ b/chrome/android/junit/src/org/chromium/chrome/browser/handoff/HandoffControllerUnitTest.java @@ -227,6 +227,21 @@ } @Test + public void testUpdateHandoffState_PdfUrl_Disabled() { + when(mTab.getUrl()) + .thenReturn( + new GURL( + "chrome-native://pdf/" + + "link?url=content%3A%2F%2Fcom.example.provider%2Ffile.pdf")); + mController = + new HandoffController( + mActivity, mTabModelSelector, mActivityTabProvider, mDelegate); + ShadowLooper.idleMainLooper(); + + verify(mDelegate, atLeastOnce()).setHandoffEnabled(eq(mActivity), eq(false)); + } + + @Test public void testOnHandoffActivityDataRequested_UnsafeUrl_ReturnsNull() throws Exception { initializeController(); when(mTab.getUrl()).thenReturn(new GURL("chrome://flags")); diff --git a/chrome/browser/ui/startup/url_util_unittest.cc b/chrome/browser/ui/startup/url_util_unittest.cc index a403d42..e232c28 100644 --- a/chrome/browser/ui/startup/url_util_unittest.cc +++ b/chrome/browser/ui/startup/url_util_unittest.cc @@ -43,6 +43,12 @@ EXPECT_FALSE(ValidateLaunchUrlWebUnsafe(GURL("about:about"))); EXPECT_FALSE(ValidateLaunchUrlWebUnsafe(GURL("about:"))); + // chrome-native: URLs should be rejected + EXPECT_FALSE(ValidateLaunchUrlWebUnsafe(GURL("chrome-native://pdf/"))); + EXPECT_FALSE(ValidateLaunchUrlWebUnsafe(GURL( + "chrome-native://pdf/link?url=https%3A%2F%2Fexample.com%2Fdoc.pdf"))); + EXPECT_FALSE(ValidateLaunchUrlWebUnsafe(GURL("chrome-native://newtab/"))); + #if BUILDFLAG(IS_ANDROID) EXPECT_TRUE(ValidateLaunchUrlWebUnsafe( GURL("content://packagename.providername/path"))); @@ -71,6 +77,12 @@ EXPECT_FALSE(ValidateLaunchUrlWebSafe(GURL("about:settings"))); EXPECT_FALSE(ValidateLaunchUrlWebSafe(GURL("about:about"))); EXPECT_FALSE(ValidateLaunchUrlWebSafe(GURL("about:"))); + + // chrome-native: URLs should be rejected in WebSafe context. + EXPECT_FALSE(ValidateLaunchUrlWebSafe(GURL("chrome-native://pdf/"))); + EXPECT_FALSE(ValidateLaunchUrlWebSafe(GURL( + "chrome-native://pdf/link?url=https%3A%2F%2Fexample.com%2Fdoc.pdf"))); + EXPECT_FALSE(ValidateLaunchUrlWebSafe(GURL("chrome-native://newtab/"))); } TEST(UrlUtilTest, ValidateUrlRejectsNestedSchemes) {
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/ui/startup/url_util_unittest.cc b/chrome/browser/ui/startup/url_util_unittest.cc
index a403d42..e232c28 100644
--- a/chrome/browser/ui/startup/url_util_unittest.cc
+++ b/chrome/browser/ui/startup/url_util_unittest.cc
@@ -43,6 +43,12 @@
EXPECT_FALSE(ValidateLaunchUrlWebUnsafe(GURL("about:about")));
EXPECT_FALSE(ValidateLaunchUrlWebUnsafe(GURL("about:")));
+ // chrome-native: URLs should be rejected
+ EXPECT_FALSE(ValidateLaunchUrlWebUnsafe(GURL("chrome-native://pdf/")));
+ EXPECT_FALSE(ValidateLaunchUrlWebUnsafe(GURL(
+ "chrome-native://pdf/link?url=https%3A%2F%2Fexample.com%2Fdoc.pdf")));
+ EXPECT_FALSE(ValidateLaunchUrlWebUnsafe(GURL("chrome-native://newtab/")));
+
#if BUILDFLAG(IS_ANDROID)
EXPECT_TRUE(ValidateLaunchUrlWebUnsafe(
GURL("content://packagename.providername/path")));
@@ -71,6 +77,12 @@
EXPECT_FALSE(ValidateLaunchUrlWebSafe(GURL("about:settings")));
EXPECT_FALSE(ValidateLaunchUrlWebSafe(GURL("about:about")));
EXPECT_FALSE(ValidateLaunchUrlWebSafe(GURL("about:")));
+
+ // chrome-native: URLs should be rejected in WebSafe context.
+ EXPECT_FALSE(ValidateLaunchUrlWebSafe(GURL("chrome-native://pdf/")));
+ EXPECT_FALSE(ValidateLaunchUrlWebSafe(GURL(
+ "chrome-native://pdf/link?url=https%3A%2F%2Fexample.com%2Fdoc.pdf")));
+ EXPECT_FALSE(ValidateLaunchUrlWebSafe(GURL("chrome-native://newtab/")));
}
TEST(UrlUtilTest, ValidateUrlRejectsNestedSchemes) {
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.
References
On This Page