Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in WebAppInstalls
DescriptionInsufficient validation of untrusted input in WebAppInstalls
ComponentWebAppInstalls
Bug ClassLogic Error
Tracker513046475
Fix commit6a7be7559127 (chromium/src) +197/-14
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
if
chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappAuthenticator.java
modified
if
chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappLauncherActivity.java
modified

Files Changed

  • chrome/android/java/src/org/chromium/chrome/browser/ShortcutHelper.java
  • chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappAuthenticator.java
  • chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappIntentDataProviderFactory.java
  • chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappLauncherActivity.java
From 6a7be7559127df8739e0670fb59d6b056d918615 Mon Sep 17 00:00:00 2001
From: Dan Murphy <[email protected]>
Date: Wed, 20 May 2026 11:47:05 -0700
Subject: [PATCH] [Webapps] Verify Webapp icon MAC in WebappAuthenticator

Verify the Webapp icon MAC in WebappAuthenticator to prevent unsandboxed
image decoding of untrusted icons.

Previously, the MAC only verified the URL. This allowed malicious apps
to pass an arbitrary icon which would be decoded in the browser process.

Now, we compute the MAC over both the URL and the icon (if present). We
introduce EXTRA_IS_ICON_TRUSTED to propagate the trust status. If the
MAC is valid for both URL and icon, the icon is trusted. If the MAC is
only valid for the URL (legacy MAC), the launch succeeds but the icon is
marked as untrusted and not decoded. If the MAC is invalid, the launch
is aborted.

TAG=agy
CONV=e0a70edd-f9f7-49d5-972f-373b9e25a429

Bug: 513046475, b:514441798
Test: run_chrome_junit_tests -f "*WebappAuthenticatorTest*"
Change-Id: Ia559ec4f80f3b95d3301fe707b9c1ea4a9023717
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7858195
Reviewed-by: Glenn Hartmann <[email protected]>
Commit-Queue: Daniel Murphy <[email protected]>
Reviewed-by: Patrick Noland <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1633734}
---

diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ShortcutHelper.java b/chrome/android/java/src/org/chromium/chrome/browser/ShortcutHelper.java
index 63ebd12a..55b6365 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ShortcutHelper.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ShortcutHelper.java
@@ -128,7 +128,7 @@
                                 backgroundColor,
                                 iconUrl.isEmpty(),
                                 isIconAdaptive);
-                shortcutIntent.putExtra(WebappConstants.EXTRA_MAC, getEncodedMac(url));
+                shortcutIntent.putExtra(WebappConstants.EXTRA_MAC, getEncodedMac(url, encodedIcon));
                 shortcutIntent.putExtra(
                         WebappConstants.EXTRA_SOURCE, ShortcutSource.ADD_TO_HOMESCREEN_STANDALONE);
                 return shortcutIntent;
@@ -258,7 +258,8 @@
                 .putExtra(WebappConstants.EXTRA_THEME_COLOR, themeColor)
                 .putExtra(WebappConstants.EXTRA_BACKGROUND_COLOR, backgroundColor)
                 .putExtra(WebappConstants.EXTRA_IS_ICON_GENERATED, isIconGenerated)
-                .putExtra(WebappConstants.EXTRA_IS_ICON_ADAPTIVE, isIconAdaptive);
+                .putExtra(WebappConstants.EXTRA_IS_ICON_ADAPTIVE, isIconAdaptive)
+                .putExtra(WebappConstants.EXTRA_IS_ICON_TRUSTED, true);
         return shortcutIntent;
     }
 
@@ -332,11 +333,11 @@
     /**
      * @return String that can be used to verify that a WebappActivity is being started by Chrome.
      */
-    public static String getEncodedMac(String url) {
+    public static String getEncodedMac(String url, @Nullable String encodedIcon) {
         // The only reason we convert to a String here is because Android inexplicably eats a
         // byte[] when adding the shortcut -- the Bundle received by the launched Activity even
         // lacks the key for the extra.
-        byte[] mac = WebappAuthenticator.getMacForUrl(url);
+        byte[] mac = WebappAuthenticator.getMacForUrlAndIcon(url, encodedIcon);
         return Base64.encodeToString(mac, Base64.DEFAULT);
     }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappAuthenticator.java b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappAuthenticator.java
index fcc690b..d463359 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappAuthenticator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappAuthenticator.java
@@ -42,6 +42,10 @@
     private static final int MAC_KEY_BYTE_COUNT = 32;
     private static final Object sLock = new Object();
 
+    public static final int MAC_INVALID = 0;
+    public static final int MAC_LEGACY = 1;
+    public static final int MAC_TRUSTED = 2;
+
     private static @Nullable SecretKey sKey;
 
     /**
@@ -67,11 +71,49 @@
      * @return The bytes of a MAC for the URL, or null if a secure MAC was not available.
      */
     public static byte @Nullable [] getMacForUrl(String url) {
+        return getMacForUrlAndIcon(url, null);
+    }
+
+    /**
+     * Calculates a MAC for the concatenation of a URL and an encoded icon.
+     *
+     * @param url A URL for which to calculate a MAC.
+     * @param encodedIcon The base64 encoded icon, or null.
+     * @return The bytes of a MAC, or null if a secure MAC was not available.
+     */
+    public static byte @Nullable [] getMacForUrlAndIcon(String url, @Nullable String encodedIcon) {
         Mac mac = getMac();
         if (mac == null) {
             return null;
         }
-        return mac.doFinal(ApiCompatibilityUtils.getBytesUtf8(url));
+        mac.update(ApiCompatibilityUtils.getBytesUtf8(url));
+        if (encodedIcon != null) {
+            mac.update(ApiCompatibilityUtils.getBytesUtf8(encodedIcon));
+        }
+        return mac.doFinal();
+    }
+
+    /**
+     * Verifies the MAC for a URL and encoded icon.
+     *
+     * @param url The URL to validate.
+     * @param encodedIcon The base64 encoded icon, or null.
+     * @param mac The bytes of a previously-calculated MAC.
+     * @return MAC_TRUSTED (2) if MAC matches URL and Icon. MAC_LEGACY (1) if MAC matches URL only.
+     *     MAC_INVALID (0) otherwise.
+     */
+    public static int verifyMac(String url, @Nullable String encodedIcon, byte[] mac) {
+        if (encodedIcon != null) {
+            byte[] goodMacWithIcon = getMacForUrlAndIcon(url, encodedIcon);
+            if (goodMacWithIcon != null && constantTimeAreArraysEqual(goodMacWithIcon, mac)) {
+                return MAC_TRUSTED;
+            }
+        }
+        byte[] goodMacUrlOnly = getMacForUrlAndIcon(url, null);
+        if (goodMacUrlOnly != null && constantTimeAreArraysEqual(goodMacUrlOnly, mac)) {
+            return MAC_LEGACY;
+        }
+        return MAC_INVALID;
     }
 
     // TODO(palmer): Put this method, and as much of this class as possible, in a utility class.
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappIntentDataProviderFactory.java b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappIntentDataProviderFactory.java
index b9092de..5981e62 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappIntentDataProviderFactory.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappIntentDataProviderFactory.java
@@ -122,6 +122,9 @@
         boolean isIconAdaptive =
                 IntentUtils.safeGetBooleanExtra(
                         intent, WebappConstants.EXTRA_IS_ICON_ADAPTIVE, false);
+        boolean isIconTrusted =
+                IntentUtils.safeGetBooleanExtra(
+                        intent, WebappConstants.EXTRA_IS_ICON_TRUSTED, false);
         boolean forceNavigation =
                 IntentUtils.safeGetBooleanExtra(
                         intent, WebappConstants.EXTRA_FORCE_NAVIGATION, false);
@@ -137,7 +140,7 @@
                         id,
                         url,
                         scope,
-                        new WebappIcon(icon, /* isTrusted= */ true),
+                        new WebappIcon(icon, isIconTrusted),
                         name,
                         shortName,
                         displayMode,
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappLauncherActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappLauncherActivity.java
index 427802e..13281cae 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappLauncherActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappLauncherActivity.java
@@ -81,6 +81,7 @@
         public final boolean isForWebApk;
         public final @Nullable String webApkPackageName;
         public final boolean isSplashProvidedByWebApk;
+        public boolean isIconTrusted;
 
         public LaunchData(
                 @Nullable String id,
@@ -251,11 +252,32 @@
         ComponentName component = intent.getComponent();
         assumeNonNull(component);
         if (component.equals(new ComponentName(appContext, SECURE_WEBAPP_LAUNCHER))) {
+            launchData.isIconTrusted = true;
+            return true;
+        }
+
+        if (wasIntentFromChrome(intent)) {
+            launchData.isIconTrusted = true;
             return true;
         }
 
         String webappMac = IntentUtils.safeGetStringExtra(intent, WebappConstants.EXTRA_MAC);
-        return (isValidMacForUrl(launchData.url, webappMac) || wasIntentFromChrome(intent));
+        if (webappMac == null) {
+            return false;
+        }
+        byte[] macBytes = Base64.decode(webappMac, Base64.DEFAULT);
+        String encodedIcon = IntentUtils.safeGetStringExtra(intent, WebappConstants.EXTRA_ICON);
+
+        int verificationResult =
+                WebappAuthenticator.verifyMac(launchData.url, encodedIcon, macBytes);
+        if (verificationResult == WebappAuthenticator.MAC_TRUSTED) {
+            launchData.isIconTrusted = true;
+            return true;
+        } else if (verificationResult == WebappAuthenticator.MAC_LEGACY) {
+            launchData.isIconTrusted = false;
+            return true;
+        }
+        return false;
Loading diff…

Original Bug Report

reported by [email protected]

Potential unsandboxed image decoding in browser process via WebappActivity EXTRA_ICON

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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: The WebappLauncherActivity validates launch intents using a MAC that only covers the URL, allowing other extras to be attacker-controlled. WebappIntentDataProviderFactory then hardcodes the ‘isTrusted’ flag for the icon provided in the intent, leading to its decoding via native codecs in the unsandboxed browser process. This potentially violates the ‘Rule of 2’ by parsing untrusted image data in a privileged process.

Affected files:

  • chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappIntentDataProviderFactory.java
  • chrome/android/java/src/org/chromium/chrome/browser/webapps/WebappLauncherActivity.java
  • chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappIcon.java
  • chrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/BitmapHelper.java

Estimated timestamp from git blame: 2020-04-28

Summary

A potential vulnerability in the Android web app launch process allows an attacker to trigger an unsandboxed image decode in the Chrome browser process. By exploiting partial authentication of launch intents and a hardcoded trust assignment, a malicious application can supply arbitrary image bytes that are parsed by native codecs (e.g., Skia, libpng) in the privileged browser process.

Root Cause Analysis

  1. Partial Intent Authentication: WebappLauncherActivity (an exported activity) uses an HMAC-SHA256 MAC to validate launch intents. However, this MAC only authenticates the EXTRA_URL parameter (see WebappLauncherActivity.java:336 and WebappAuthenticator.java:74). Other intent extras, such as EXTRA_ICON, are copied to the internal launch intent without verification (WebappLauncherActivity.java:367).

  2. Hardcoded Trust in Factory: When WebappActivity starts, WebappIntentDataProviderFactory.create() extracts the icon from the intent and instantiates a WebappIcon object with the isTrusted flag hardcoded to true (WebappIntentDataProviderFactory.java:140):

    new WebappIcon(icon, /* isTrusted= */ true)
    
  3. Unsandboxed Native Decode: During activity startup, the splash screen construction (via WebappSplashController) triggers WebappIcon.bitmap(). Because the icon is marked as trusted, it proceeds to decode the base64-encoded string using native codecs in the browser process:

    // WebappIcon.java:127
    return BitmapHelper.decodeBitmapFromString(mEncoded);
    
    // BitmapHelper.java:43
    return BitmapFactory.decodeByteArray(decoded, 0, decoded.length);
    

Potential Attack Vector

An attacker would follow these suggested steps:

  1. Obtain a valid (URL, MAC) pair for a Chrome installation (e.g., from a leaked shortcut intent or a malicious launcher).
  2. From a co-installed malicious app, send an intent to org.chromium.chrome.browser.webapps.WebappLauncherActivity with action com.google.android.apps.chrome.webapps.WebappManager.ACTION_START_WEBAPP.
  3. Include the valid EXTRA_URL and EXTRA_MAC to pass the launcher’s initial check.
  4. Include a non-existent EXTRA_ID (to ensure the app falls back to the intent-provided icon) and a malicious EXTRA_ICON containing base64-encoded bytes targeting a native codec vulnerability.
  5. Upon launch, Chrome will decode the malicious bytes in the unsandboxed browser process during splash screen initialization.

Suggested Fix

  1. Extend MAC Coverage: Update the WebappAuthenticator to include EXTRA_ICON (and other sensitive extras) in the MAC calculation.
  2. Remove Hardcoded Trust: Do not hardcode isTrusted = true in WebappIntentDataProviderFactory.create(). Instead, icons provided via intents from external apps should be treated as untrusted by default, or trust should only be granted if the MAC explicitly covers the icon data.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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