CVE-2026-19143
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateImpl.java |
modified | |
forcomponents/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkValidator.java |
modified | |
ifcomponents/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkValidator.java |
modified | |
switchcomponents/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkValidator.java |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateImpl.javachrome/android/junit/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateUnitTest.javacomponents/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkValidator.java
Patch
From e9063d0f2ef678334d1d33478f76375ca92e203e Mon Sep 17 00:00:00 2001 From: Nate Chapin <[email protected]> Date: Tue, 21 Jul 2026 10:01:39 -0700 Subject: [PATCH] [webapk] Restrict WebAPK URL resolution to the requested package createWebApkIntentForUrlAndOptionalPackage() called Intent#setPackage() on the main intent even when Intent#parseUri() had produced a selector. Android rejects setting a package while a selector is present, throwing IllegalArgumentException, and PackageManager resolves against the selector intent which was never package-restricted. Set the package on the selector when one is present, otherwise on the main intent. Also have canWebApkHandleUrl() ignore ResolveInfos whose package does not match the requested webApkPackage so the function only considers the package the caller asked about. TAG=agy CONV=6dfa2923-2a49-4f39-8271-c4c428be0bd0 Fixed: 517772612 Change-Id: I9e0d05566c282c628748f328eae5c00a3ee627fc Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8111999 Reviewed-by: Michael Thiessen <[email protected]> Reviewed-by: Glenn Hartmann <[email protected]> Commit-Queue: Nate Chapin <[email protected]> Cr-Commit-Position: refs/heads/main@{#1665577} --- diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateImpl.java index 4579f813..dc6ad279 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateImpl.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateImpl.java @@ -660,11 +660,16 @@ public static @Nullable ResolveInfo queryWebApkResolveInfo(Context context, Tab currentTab) { String manifestId = AppBannerManager.maybeGetManifestId(assumeNonNull(currentTab.getWebContents())); + String expectedPackage = WebappRegistry.getInstance().findWebApkWithManifestId(manifestId); ResolveInfo resolveInfo = WebApkValidator.queryFirstWebApkResolveInfo( - context, - currentTab.getUrl().getSpec(), - WebappRegistry.getInstance().findWebApkWithManifestId(manifestId)); + context, currentTab.getUrl().getSpec(), expectedPackage); + + if (resolveInfo != null + && expectedPackage != null + && !expectedPackage.equals(resolveInfo.activityInfo.packageName)) { + resolveInfo = null; + } if (resolveInfo == null) { // If a WebAPK with matching manifestId can't be found, fallback to query without it. diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateUnitTest.java index 3750ace..6f74445 100644 --- a/chrome/android/junit/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateUnitTest.java +++ b/chrome/android/junit/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateUnitTest.java @@ -18,6 +18,11 @@ import static org.mockito.Mockito.when; import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.view.ContextThemeWrapper; import android.view.View; @@ -30,7 +35,9 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import org.robolectric.Shadows; import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowPackageManager; import org.chromium.base.ContextUtils; import org.chromium.base.supplier.ObservableSuppliers; @@ -64,6 +71,7 @@ import org.chromium.chrome.browser.translate.TranslateBridge; import org.chromium.chrome.browser.translate.TranslateBridgeJni; import org.chromium.chrome.browser.ui.appmenu.AppMenuItemProperties; +import org.chromium.chrome.browser.webapps.WebappDataStorage; import org.chromium.chrome.browser.webapps.WebappRegistry; import org.chromium.components.bookmarks.BookmarkId; import org.chromium.components.browser_ui.accessibility.PageZoomUtils; @@ -81,6 +89,7 @@ import org.chromium.components.prefs.PrefService; import org.chromium.components.user_prefs.UserPrefs; import org.chromium.components.user_prefs.UserPrefsJni; +import org.chromium.components.webapk.lib.client.WebApkValidator; import org.chromium.components.webapps.AppBannerManager; import org.chromium.components.webapps.AppBannerManagerJni; import org.chromium.content_public.browser.NavigationController; @@ -90,6 +99,7 @@ import org.chromium.ui.modelutil.PropertyModel; import org.chromium.url.GURL; import org.chromium.url.JUnitTestGURLs; +import org.chromium.webapk.lib.common.WebApkConstants; import java.util.ArrayList; import java.util.List; @@ -502,6 +512,71 @@ setUpIncognitoMocks(); } + @Test + public void testQueryWebApkResolveInfoFiltersMismatchedPackage() { + String manifestId = "https://example.com/manifest"; + String expectedPackage = "org.chromium.webapk.expected"; + String mismatchedPackage = "org.chromium.webapk.mismatched"; + String url = "https://example.com/start"; + + AppBannerManager.Natives appBannerManagerJniMock = mock(AppBannerManager.Natives.class); + AppBannerManagerJni.setInstanceForTesting(appBannerManagerJniMock); + when(appBannerManagerJniMock.getInstallableWebAppManifestId(any())).thenReturn(manifestId); + + Context context = ContextUtils.getApplicationContext(); + SharedPreferences prefs = + context.getSharedPreferences("webapp_registry", Context.MODE_PRIVATE); + java.util.Set<String> webapps = new java.util.HashSet<>(); + String webapkId = WebApkConstants.WEBAPK_ID_PREFIX + expectedPackage; + webapps.add(webapkId); + prefs.edit().putStringSet("webapp_set", webapps).apply(); + + WebappDataStorage mockedStorage = mock(WebappDataStorage.class); + when(mockedStorage.getId()).thenReturn(webapkId); + when(mockedStorage.getWebApkPackageName()).thenReturn(expectedPackage); + when(mockedStorage.getWebApkManifestId()).thenReturn(manifestId); + when(mockedStorage.getScope()).thenReturn("https://example.com/"); + + WebappDataStorage.setFactoryForTests( + new WebappDataStorage.Factory() { + @Override + public WebappDataStorage create(String id) { + if (id.equals(webapkId)) { + return mockedStorage; + } + return super.create(id); + } + }); + + WebappRegistry.refreshSharedPrefsForTesting(); + RobolectricUtil.runAllBackgroundAndUi(); + + assertEquals( + expectedPackage, WebappRegistry.getInstance().findWebApkWithManifestId(manifestId)); + + when(mTab.getUrl()).thenReturn(new GURL(url)); + + PackageManager pm = context.getPackageManager(); + ShadowPackageManager shadowPm = Shadows.shadowOf(pm); + + Intent constrainedIntent = new Intent(Intent.ACTION_VIEW, android.net.Uri.parse(url)); + constrainedIntent.addCategory(Intent.CATEGORY_BROWSABLE); + constrainedIntent.setPackage(expectedPackage); + + ResolveInfo mismatchedResolveInfo = new ResolveInfo(); + mismatchedResolveInfo.activityInfo = new ActivityInfo(); + mismatchedResolveInfo.activityInfo.packageName = mismatchedPackage; + mismatchedResolveInfo.activityInfo.name = "MainActivity"; + + shadowPm.addResolveInfoForIntent(constrainedIntent, mismatchedResolveInfo); + + WebApkValidator.setDisableValidationForTesting(true); + + ResolveInfo result = AppMenuPropertiesDelegateImpl.queryWebApkResolveInfo(context, mTab); + + assertNull(result); + } + private void setUpIncognitoMocks() { doReturn(true).when(mAppMenuPropertiesDelegate).isIncognitoEnabled(); } diff --git a/components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkValidator.java b/components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkValidator.java index 3b2308e..aedaf56 100644 --- a/components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkValidator.java +++ b/components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkValidator.java @@ -176,6 +176,9 @@ List<ResolveInfo> infos = resolveInfosForUrlAndOptionalPackage(context, url, webApkPackage); for (ResolveInfo info : infos) { if (info.activityInfo != null) { + if (!info.activityInfo.packageName.equals(webApkPackage)) { + continue; + } @ValidationResult int result = isValidWebApkInternal(context, info.activityInfo.packageName); switch (result) { @@ -364,14 +367,10 @@ intent.addCategory(Intent.CATEGORY_BROWSABLE); intent.setComponent(null); + intent.setSelector(null); if (applicationPackage != null) { intent.setPackage(applicationPackage); } - Intent selector = intent.getSelector(); - if (selector != null) { - selector.addCategory(Intent.CATEGORY_BROWSABLE); - selector.setComponent(null);
Original Bug Report
Potential WebApkValidator signature bypass leads to in-process image decoding in Browser process
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 potential vulnerability in Chrome for Android allows a co-installed malicious app to bypass signature validation in WebApkValidator by supplying an intent URI with an explicit component. This bypass permits the attacker’s package to launch SameTaskWebApkActivity and trigger custom splash screen retrieval. Consequently, the unsandboxed Browser process decodes an attacker-controlled image in-process via BitmapFactory, violating the Rule of 2.
Affected files:
chrome/android/java/src/org/chromium/chrome/browser/browserservices/ui/splashscreen/webapps/WebappSplashController.javacomponents/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkValidator.javabase/android/java/src/org/chromium/base/FileUtils.java
Estimated timestamp from git blame: 2017-06-30
Detailed Writeup
1. The Package Validation Bypass in WebApkValidator
In components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkValidator.java, the helper method createWebApkIntentForUrlAndOptionalPackage parses a user-provided intent URL and restricts the intent to a specific WebAPK package if specified:
public static @Nullable Intent createWebApkIntentForUrlAndOptionalPackage(
String url, @Nullable String applicationPackage) {
Intent intent;
try {
intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
} catch (Exception e) {
return null;
}
intent.addCategory(Intent.CATEGORY_BROWSABLE);
if (applicationPackage != null) {
intent.setPackage(applicationPackage);
} else {
intent.setComponent(null);
}
...
If the input url contains an explicit component pointing to a legitimate WebAPK (e.g., component=org.chromium.webapk.legit_pwa/...), the applicationPackage != null branch is executed. This sets the package filter to the attacker’s package (com.attacker) but fails to clear the explicit component.
When PackageManager.queryIntentActivities is subsequently called to resolve the intent, the Android framework respects the explicit component and ignores the conflicting package filter. Thus, the resolved ResolveInfo corresponds to the legitimate WebAPK, and its signature is verified successfully. However, Chrome downstream accepts the unvalidated com.attacker package name for further execution.
2. In-Process Native Image Decoding (Rule of 2 Violation)
Upon successful validation bypass, WebappLauncherActivity launches SameTaskWebApkActivity using the unvalidated package name.
During startup, WebappSplashController.java attempts to build the splash screen by querying the WebAPK’s content provider:
String packageName = mWebappInfo.webApkPackageName();
Bitmap splashBitmap =
FileUtils.queryBitmapFromContentProvider(
appContext,
Uri.parse(WebApkCommonUtils.generateSplashContentProviderUri(packageName)));
In base/android/java/src/org/chromium/base/FileUtils.java, this resolves the content provider URI (content://com.attacker.SplashContentProvider/cached_splash_image) and decodes the resulting file descriptor:
public static @Nullable Bitmap queryBitmapFromContentProvider(Context context, Uri uri) {
try (ParcelFileDescriptor parcelFileDescriptor =
context.getContentResolver().openFileDescriptor(uri, "r")) {
...
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
Because the decoding is performed using BitmapFactory.decodeFileDescriptor directly in the highly privileged Browser process rather than the sandboxed data_decoder process, this constitutes a direct “Rule of 2” violation. An attacker could exploit a platform-level vulnerability within native image codecs (such as libwebp or Skia) to execute arbitrary code with Browser process privileges, escaping the sandbox entirely.
Potential Attack Scenario
- A co-installed malicious app (
com.attacker) registers aContentProviderundercom.attacker.SplashContentProviderthat serves a malformed image payload. - The malicious app fires an intent to the exported
WebappLauncherActivitycontaining:EXTRA_WEBAPK_PACKAGE_NAME="com.attacker"EXTRA_SPLASH_PROVIDED_BY_WEBAPK=trueEXTRA_URL="intent:#Intent;component=org.chromium.webapk.legit_pwa/org.chromium.webapk.shell_apk.h2o.H2OOpaqueMainActivity;end"
- Chrome validates the intent. Due to the explicit component mismatch, the signature validation passes by verifying the legitimate WebAPK (
org.chromium.webapk.legit_pwa). - Chrome launches
SameTaskWebApkActivityand callsFileUtils.queryBitmapFromContentProviderto query the attacker’s custom splash screen content provider. BitmapFactory.decodeFileDescriptordecodes the malicious image in-process within Chrome’s Browser process, potentially triggering memory corruption.
Note: These are potential steps. Our tooling does not currently have the capability to run code or provide a functional proof-of-concept exploit.
Suggested Remediation
Explicitly clear the component in createWebApkIntentForUrlAndOptionalPackage when an applicationPackage restriction is applied:
intent.addCategory(Intent.CATEGORY_BROWSABLE);
if (applicationPackage != null) {
intent.setPackage(applicationPackage);
intent.setComponent(null); // FIX: Ensure explicit components cannot bypass package limits
} else {
intent.setComponent(null);
}
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
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.