CVE-2026-9987
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/webapps/WebappAuthenticator.java |
modified | |
ifchrome/android/java/src/org/chromium/chrome/browser/webapps/WebappLauncherActivity.java |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/ShortcutHelper.javachrome/android/java/src/org/chromium/chrome/browser/webapps/WebappAuthenticator.javachrome/android/java/src/org/chromium/chrome/browser/webapps/WebappIntentDataProviderFactory.javachrome/android/java/src/org/chromium/chrome/browser/webapps/WebappLauncherActivity.java
Patch
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;
Original Bug Report
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.javachrome/android/java/src/org/chromium/chrome/browser/webapps/WebappLauncherActivity.javachrome/browser/android/browserservices/intents/java/src/org/chromium/chrome/browser/browserservices/intents/WebappIcon.javachrome/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
-
Partial Intent Authentication:
WebappLauncherActivity(an exported activity) uses an HMAC-SHA256 MAC to validate launch intents. However, this MAC only authenticates theEXTRA_URLparameter (seeWebappLauncherActivity.java:336andWebappAuthenticator.java:74). Other intent extras, such asEXTRA_ICON, are copied to the internal launch intent without verification (WebappLauncherActivity.java:367). -
Hardcoded Trust in Factory: When
WebappActivitystarts,WebappIntentDataProviderFactory.create()extracts the icon from the intent and instantiates aWebappIconobject with theisTrustedflag hardcoded totrue(WebappIntentDataProviderFactory.java:140):new WebappIcon(icon, /* isTrusted= */ true) -
Unsandboxed Native Decode: During activity startup, the splash screen construction (via
WebappSplashController) triggersWebappIcon.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:
- Obtain a valid
(URL, MAC)pair for a Chrome installation (e.g., from a leaked shortcut intent or a malicious launcher). - From a co-installed malicious app, send an intent to
org.chromium.chrome.browser.webapps.WebappLauncherActivitywith actioncom.google.android.apps.chrome.webapps.WebappManager.ACTION_START_WEBAPP. - Include the valid
EXTRA_URLandEXTRA_MACto pass the launcher’s initial check. - Include a non-existent
EXTRA_ID(to ensure the app falls back to the intent-provided icon) and a maliciousEXTRA_ICONcontaining base64-encoded bytes targeting a native codec vulnerability. - Upon launch, Chrome will decode the malicious bytes in the unsandboxed browser process during splash screen initialization.
Suggested Fix
- Extend MAC Coverage: Update the
WebappAuthenticatorto includeEXTRA_ICON(and other sensitive extras) in the MAC calculation. - Remove Hardcoded Trust: Do not hardcode
isTrusted = trueinWebappIntentDataProviderFactory.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.