Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in WebAPKs
DescriptionInsufficient validation of untrusted input in WebAPKs
ComponentWebAPKs
Bug ClassLogic Error
Tracker517772612
Fix commite9063d0f2ef6 (chromium/src) +142/-8
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-06

Changed Functions

FunctionChangeNotes
if
chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateImpl.java
modified
for
components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkValidator.java
modified
if
components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkValidator.java
modified
switch
components/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.java
  • chrome/android/junit/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateUnitTest.java
  • components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkValidator.java
From e9063d0f2ef678334d1d33478f76375ca92e203e Mon Sep 17 00:00:00 2001
From: Nate Chapin <japhet@chromium.org>
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 <mthiesse@chromium.org>
Reviewed-by: Glenn Hartmann <hartmanng@chromium.org>
Commit-Queue: Nate Chapin <japhet@chromium.org>
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);
Loading diff…

Original Bug Report

reported by vm...@google.com

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.java
  • components/webapk/android/libs/client/src/org/chromium/components/webapk/lib/client/WebApkValidator.java
  • base/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

  1. A co-installed malicious app (com.attacker) registers a ContentProvider under com.attacker.SplashContentProvider that serves a malformed image payload.
  2. The malicious app fires an intent to the exported WebappLauncherActivity containing:
    • EXTRA_WEBAPK_PACKAGE_NAME = "com.attacker"
    • EXTRA_SPLASH_PROVIDED_BY_WEBAPK = true
    • EXTRA_URL = "intent:#Intent;component=org.chromium.webapk.legit_pwa/org.chromium.webapk.shell_apk.h2o.H2OOpaqueMainActivity;end"
  3. Chrome validates the intent. Due to the explicit component mismatch, the signature validation passes by verifying the legitimate WebAPK (org.chromium.webapk.legit_pwa).
  4. Chrome launches SameTaskWebApkActivity and calls FileUtils.queryBitmapFromContentProvider to query the attacker’s custom splash screen content provider.
  5. BitmapFactory.decodeFileDescriptor decodes 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.

View on issue tracker