Medium chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in WebShare
DescriptionImproper input validation in WebShare
ComponentWebShare
Sub-componentAndroid
Bug ClassInput Validation
Tracker501531546
Fix commit8714b2860d64 (chromium/src) +89/-42
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
base/android/java/src/org/chromium/base/ApiCompatibilityUtils.java
modified

Files Changed

  • chrome/browser/share/android/java/src/org/chromium/chrome/browser/share/screenshot/ScreenshotShareSheetMediator.java
  • chrome/browser/share/android/java/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContent.java
  • base/android/java/src/org/chromium/base/ApiCompatibilityUtils.java
  • chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContentTest.java
  • components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareImageFileUtils.java
From 8714b2860d6419d7722e24b168d785192819edf2 Mon Sep 17 00:00:00 2001
From: Adriana Ixba <aixba@chromium.org>
Date: Thu, 17 Sep 2026 15:42:41 -0700
Subject: [PATCH] [Android] Sharing Hub: use placeholder icon for image previews

ShareSheetBottomSheetContent previously decoded shared image files
in-process via android.graphics.ImageDecoder to generate a preview
thumbnail. To adhere to the Rule of 2 and prevent unsandboxed decoding
of untrusted renderer-supplied image bytes (from both navigator.share()
and context-menu image sharing), replace this with the generic file
placeholder icon. Also remove
ShareImageFileUtils.getBitmapFromUriAsync(), which is now unused.

Buffering arbitrary shared files in memory and routing them across JNI
and IPC to data_decoder::DecodeImageIsolated adds unwarranted complexity
and overhead for a legacy UI path that is only used on Android <= 13 and
Automotive devices without a system sharesheet (phased out on Android
14+ in favor of the OS sharesheet). Using the placeholder icon matches
how other shared file types (audio, video, PDF) are already handled in
the Sharing Hub.

before: https://screenshot-v2.corp.google.com/58t91j98735eg
after: https://screenshot-v2.corp.google.com/30j57qpi04f5o
Bug: 501531546
Change-Id: I16b0717af9353dec34022162b02ec9a64b425f1f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8401809
Commit-Queue: Adriana Ixba <aixba@chromium.org>
Reviewed-by: Wenyu Fu <wenyufu@chromium.org>
Reviewed-by: Calder Kitagawa <ckitagawa@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1700199}
---
diff --git a/base/android/java/src/org/chromium/base/ApiCompatibilityUtils.java b/base/android/java/src/org/chromium/base/ApiCompatibilityUtils.java
index fa36272b..bed0cfdf 100644
--- a/base/android/java/src/org/chromium/base/ApiCompatibilityUtils.java
+++ b/base/android/java/src/org/chromium/base/ApiCompatibilityUtils.java
@@ -213,7 +213,13 @@
         return false;
     }
 
-    /** Retrieves an image for the given uri as a Bitmap. */
+    /**
+     * Retrieves an image for the given uri as a Bitmap.
+     *
+     * <p>WARNING: Decodes in-process via {@link ImageDecoder}. Do NOT call in the browser process
+     * on untrusted or renderer-supplied URIs (violates Rule of 2; use {@code
+     * data_decoder::DecodeImageIsolated} instead).
+     */
     public static Bitmap getBitmapByUri(ContentResolver cr, Uri uri) throws IOException {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
             return ImageDecoder.decodeBitmap(ImageDecoder.createSource(cr, uri));
diff --git a/chrome/browser/share/android/java/src/org/chromium/chrome/browser/share/screenshot/ScreenshotShareSheetMediator.java b/chrome/browser/share/android/java/src/org/chromium/chrome/browser/share/screenshot/ScreenshotShareSheetMediator.java
index 5ebec5d..fb719446a 100644
--- a/chrome/browser/share/android/java/src/org/chromium/chrome/browser/share/screenshot/ScreenshotShareSheetMediator.java
+++ b/chrome/browser/share/android/java/src/org/chromium/chrome/browser/share/screenshot/ScreenshotShareSheetMediator.java
@@ -109,6 +109,7 @@
                     ShareParams params =
                             new ShareParams.Builder(mWindowAndroid, title, /* url= */ "")
                                     .setSingleImageUri(bitmapUri)
+                                    .setPreviewImageBitmap(bitmap)
                                     .setFileContentType(type)
                                     .build();
 
diff --git a/chrome/browser/share/android/java/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContent.java b/chrome/browser/share/android/java/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContent.java
index 787f9029..912af01 100644
--- a/chrome/browser/share/android/java/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContent.java
+++ b/chrome/browser/share/android/java/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContent.java
@@ -12,7 +12,6 @@
 import android.graphics.Bitmap;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
-import android.net.Uri;
 import android.os.Handler;
 import android.os.Looper;
 import android.text.TextUtils;
@@ -51,7 +50,6 @@
 import org.chromium.chrome.browser.user_education.UserEducationHelper;
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
 import org.chromium.components.browser_ui.bottomsheet.BottomSheetType;
-import org.chromium.components.browser_ui.share.ShareImageFileUtils;
 import org.chromium.components.browser_ui.share.ShareParams;
 import org.chromium.components.browser_ui.styles.SemanticColorUtils;
 import org.chromium.components.browser_ui.widget.RoundedCornerImageView;
@@ -284,8 +282,12 @@
 
         if (contentTypes.contains(ContentType.IMAGE)
                 || contentTypes.contains(ContentType.IMAGE_AND_LINK)) {
-            assert mParams.getImageUriToShare() != null;
-            setImageForPreviewFromUri(mParams.getImageUriToShare());
+            if (mParams.getPreviewImageBitmap() != null) {
+                setImageForPreviewFromBitmap(mParams.getPreviewImageBitmap());
+            } else {
+                setDefaultIconForPreview(
+                        AppCompatResources.getDrawable(mActivity, R.drawable.generic_file));
+            }
             if (TextUtils.isEmpty(subtitle)) {
                 subtitle = getFileType(fileContentType);
             }
@@ -329,11 +331,6 @@
         setTextForPreview(title, subtitle);
     }
 
-    private void setImageForPreviewFromUri(Uri imageUri) {
-        ShareImageFileUtils.getBitmapFromUriAsync(
-                mActivity, imageUri, this::setImageForPreviewFromBitmap);
-    }
-
     private void setImageForPreviewFromBitmap(@Nullable Bitmap bitmap) {
         // If no image preview available, don't show a preview.
         if (bitmap == null) return;
diff --git a/chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContentTest.java b/chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContentTest.java
index a1eb72e..c300c707 100644
--- a/chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContentTest.java
+++ b/chrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContentTest.java
@@ -153,8 +153,11 @@
                 shareSheetBottomSheetContent.getContentView().findViewById(R.id.title_preview);
         TextView subtitleView =
                 shareSheetBottomSheetContent.getContentView().findViewById(R.id.subtitle_preview);
+        ImageView imageView =
+                shareSheetBottomSheetContent.getContentView().findViewById(R.id.image_preview);
         assertEquals("", titleView.getText());
         assertEquals("image", subtitleView.getText());
+        assertNotNull(imageView.getDrawable());
     }
 
     @Test
@@ -185,8 +188,11 @@
                 shareSheetBottomSheetContent.getContentView().findViewById(R.id.title_preview);
         TextView subtitleView =
                 shareSheetBottomSheetContent.getContentView().findViewById(R.id.subtitle_preview);
+        ImageView imageView =
+                shareSheetBottomSheetContent.getContentView().findViewById(R.id.image_preview);
         assertEquals("", titleView.getText());
         assertEquals("image", subtitleView.getText());
+        assertNotNull(imageView.getDrawable());
     }
 
     @Test
@@ -220,8 +226,77 @@
                 shareSheetBottomSheetContent.getContentView().findViewById(R.id.title_preview);
         TextView subtitleView =
                 shareSheetBottomSheetContent.getContentView().findViewById(R.id.subtitle_preview);
+        ImageView imageView =
+                shareSheetBottomSheetContent.getContentView().findViewById(R.id.image_preview);
         assertEquals("", titleView.getText());
         assertEquals("video", subtitleView.getText());
+        assertNotNull(imageView.getDrawable());
+    }
+
+    @Test
+    @MediumTest
+    public void createRecyclerViews_imageAndLinkShare() {
+        String fileContentType = "image/jpeg";
+        ShareSheetBottomSheetContent shareSheetBottomSheetContent =
+                new ShareSheetBottomSheetContent(
+                        mActivity,
+                        mProfile,
+                        new MockLargeIconBridge(),
+                        null,
+                        new ShareParams.Builder(/* window= */ null, /* title= */ "", sUrl)
+                                .setSingleImageUri(sImageUri)
+                                .setFileContentType(fileContentType)
+                                .build(),
+                        mFeatureEngagementTracker);
+
+        shareSheetBottomSheetContent.createRecyclerViews(
+                ImmutableList.of(),
+                ImmutableList.of(),
+                ImmutableSet.of(ContentType.IMAGE_AND_LINK),
+                fileContentType,
+                DetailedContentType.IMAGE,
+                mShareSheetLinkToggleCoordinator);
+
+        TextView titleView =
+                shareSheetBottomSheetContent.getContentView().findViewById(R.id.title_preview);
+        TextView subtitleView =
+                shareSheetBottomSheetContent.getContentView().findViewById(R.id.subtitle_preview);
+        ImageView imageView =
+                shareSheetBottomSheetContent.getContentView().findViewById(R.id.image_preview);
+        assertEquals(View.GONE, titleView.getVisibility());
+        assertEquals(mPreviewUrl, subtitleView.getText());
+        assertNotNull(imageView.getDrawable());
+    }
+
+    @Test
+    @MediumTest
+    public void createRecyclerViews_imageWithPreviewBitmapShare() {
+        Bitmap testBitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
+        String fileContentType = "image/jpeg";
+        ShareSheetBottomSheetContent shareSheetBottomSheetContent =
+                new ShareSheetBottomSheetContent(
+                        mActivity,
+                        mProfile,
+                        new MockLargeIconBridge(),
+                        null,
+                        new ShareParams.Builder(/* window= */ null, /* title= */ "", /* url= */ "")
+                                .setSingleImageUri(sImageUri)
+                                .setPreviewImageBitmap(testBitmap)
+                                .setFileContentType(fileContentType)
+                                .build(),
+                        mFeatureEngagementTracker);
+
+        shareSheetBottomSheetContent.createRecyclerViews(
+                ImmutableList.of(),
+                ImmutableList.of(),
+                ImmutableSet.of(ContentType.IMAGE),
+                fileContentType,
+                DetailedContentType.IMAGE,
+                mShareSheetLinkToggleCoordinator);
+
+        ImageView imageView =
+                shareSheetBottomSheetContent.getContentView().findViewById(R.id.image_preview);
+        assertNotNull(imageView.getDrawable());
     }
 
     @Test
diff --git a/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareImageFileUtils.java b/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareImageFileUtils.java
index 3844d70..cf5b507 100644
--- a/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareImageFileUtils.java
+++ b/components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareImageFileUtils.java
@@ -8,17 +8,13 @@
 
 import android.content.ContentResolver;
 import android.content.ContentValues;
-import android.content.Context;
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Environment;
-import android.os.Handler;
-import android.os.Looper;
 import android.provider.MediaStore;
 
 import androidx.annotation.VisibleForTesting;
 
-import org.chromium.base.ApiCompatibilityUtils;
 import org.chromium.base.ApplicationState;
 import org.chromium.base.ApplicationStatus;
 import org.chromium.base.Callback;
@@ -29,7 +25,6 @@
 import org.chromium.base.ResettersForTesting;
 import org.chromium.base.StreamUtil;
 import org.chromium.base.task.AsyncTask;
-import org.chromium.base.task.BackgroundOnlyAsyncTask;
 import org.chromium.base.task.PostTask;
 import org.chromium.base.task.TaskTraits;
 import org.chromium.build.annotations.Contract;
@@ -218,33 +213,6 @@
                 bitmap.hasAlpha() ? PNG_EXTENSION : JPEG_EXTENSION);
     }
 
-    public static void getBitmapFromUriAsync(
-            Context context, Uri imageUri, Callback<@Nullable Bitmap> callback) {
-        new BackgroundOnlyAsyncTask<Void>() {
-            @Override
-            protected Void doInBackground() {
-                Bitmap bitmap = null;
-                try {
-                    bitmap =
-                            ApiCompatibilityUtils.getBitmapByUri(
-                                    context.getContentResolver(), imageUri);
-                    if (isHardwareBitmap(bitmap)) {
-                        bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, /* mutable= */ false);
-                    }
-                } catch (IOException e) {
-                }
-                final Bitmap result = bitmap;
-                // Run the callback on main thread.
-                new Handler(Looper.getMainLooper()).post(callback.bind(result));
-                return null;
-            }
-        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
-    }
-
-    private static boolean isHardwareBitmap(Bitmap bitmap) {
-        return bitmap.getConfig() == Bitmap.Config.HARDWARE;
-    }
-
     /** Interface for notifying image download result. */
     public interface OnImageSaveListener {
         void onImageSaved(Uri uri, String displayName);
Loading diff…

Original Bug Report

reported by vm...@google.com

Sandbox Escape via Unsandboxed Image Decoding in Android Sharing Hub

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.

Overview: A compromised renderer can trigger the Android Sharing Hub silently via the ShareService Mojo interface due to a missing user activation check. The browser process saves the supplied image and decodes it in-process using Android system APIs to generate a UI preview. This allows an attacker to exploit vulnerabilities in native system codecs (e.g., libwebp) to achieve remote code execution in the unsandboxed browser process.

Affected files:

  • components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/ShareServiceImpl.java
  • chrome/browser/share/android/java/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContent.java
  • components/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareImageFileUtils.java
  • base/android/java/src/org/chromium/base/ApiCompatibilityUtils.java
  • components/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/BlobReceiver.java
  • chrome/android/java/src/org/chromium/chrome/browser/share/ShareDelegateImpl.java
  • chrome/browser/chrome_browser_interface_binders.cc

Estimated timestamp from git blame: 2025-08-07

Summary

On Chrome for Android (specifically Android 13 and earlier), the Sharing Hub generates image previews by decoding renderer-supplied bytes directly within the browser process. This is accomplished using the Android framework’s ImageDecoder. Decoding untrusted image data in a highly privileged process violates Chrome’s “Rule of 2”. A compromised renderer can potentially exploit this architectural flaw to achieve arbitrary code execution in the browser process, resulting in a full sandbox escape, by supplying malformed image bytes that trigger vulnerabilities in Android’s native image codecs.

Technical Details

  1. Missing User Activation Check: The blink.mojom.ShareService Mojo interface is implemented in ShareServiceImpl.java within the browser process. The implementation lacks a check for transient user activation (e.g., RenderFrameHost::HasTransientUserActivation()). Consequently, a compromised renderer can trigger the sharing flow programmatically without any user interaction.
  2. Unvalidated Data Persistence: When the renderer calls the Mojo Share method with a file, ShareServiceImpl validates the file against an allowlist of extensions and MIME types. An attacker can easily spoof these (e.g., by sending a malformed WebP file disguised as an image/png with a .png extension). BlobReceiver.java then writes the renderer-supplied bytes verbatim to a temporary file in the browser’s shared files directory. No safe out-of-process transcoding or content sniffing is performed.
  3. Unsandboxed Decoding: To display the Sharing Hub bottom sheet, ShareSheetBottomSheetContent.createPreview() is called to generate a thumbnail. For image MIME types, it invokes ShareImageFileUtils.getBitmapFromUriAsync(), which delegates to ApiCompatibilityUtils.getBitmapByUri(). On Android P and above, this calls android.graphics.ImageDecoder.decodeBitmap().
  4. Sandbox Escape: ImageDecoder.decodeBitmap() inspects the magic bytes of the file, determines its actual format, and routes it to the corresponding native system codec (e.g., libwebp, libheif). Because this system API is invoked directly by the browser process, the native codec executes in-process. Exploiting a memory corruption vulnerability within the system codec allows an attacker to hijack the execution flow of the unsandboxed browser process.

Suggested Exploitation Steps

Note: These are potential steps based on code analysis; a working proof-of-concept has not been executed.

  1. An attacker gains arbitrary code execution within the sandboxed renderer process.
  2. The compromised renderer connects to the blink.mojom.ShareService interface.
  3. The attacker crafts a payload exploiting a known vulnerability in an Android native image codec (e.g., CVE-2023-4863 in libwebp).
  4. The payload is encapsulated in a SharedFile object, with its extension spoofed to .png and MIME type spoofed to image/png to bypass initial validation in ShareServiceImpl.
  5. The renderer sends the Share IPC message. The lack of a user activation check permits this action to proceed silently.
  6. The browser process saves the malformed file and attempts to generate a preview using ImageDecoder.decodeBitmap().
  7. The Android system codec parses the malformed data in-process, triggering the vulnerability and granting the attacker RCE in the browser process.

Suggested Fix

  1. Enforce User Activation: Add a HasTransientUserActivation() check in the browser process, either during the Mojo interface binding in ForwardToJavaWebContents or within ShareServiceImpl.share(), to ensure the sharing flow requires legitimate user interaction.
  2. Isolate Image Decoding: Adhere to the “Rule of 2” by moving the decoding of untrusted images for the Sharing Hub preview into an isolated utility process, or rely on safely pre-decoded bitmaps passed from the renderer.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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