CVE-2026-113147
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifbase/android/java/src/org/chromium/base/ApiCompatibilityUtils.java |
modified |
Files Changed
chrome/browser/share/android/java/src/org/chromium/chrome/browser/share/screenshot/ScreenshotShareSheetMediator.javachrome/browser/share/android/java/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContent.javabase/android/java/src/org/chromium/base/ApiCompatibilityUtils.javachrome/browser/share/android/javatests/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContentTest.javacomponents/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareImageFileUtils.java
Patch
From 8714b2860d6419d7722e24b168d785192819edf2 Mon Sep 17 00:00:00 2001 From: Adriana Ixba <[email protected]> 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 <[email protected]> Reviewed-by: Wenyu Fu <[email protected]> Reviewed-by: Calder Kitagawa <[email protected]> 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);
Original Bug Report
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.javachrome/browser/share/android/java/src/org/chromium/chrome/browser/share/share_sheet/ShareSheetBottomSheetContent.javacomponents/browser_ui/share/android/java/src/org/chromium/components/browser_ui/share/ShareImageFileUtils.javabase/android/java/src/org/chromium/base/ApiCompatibilityUtils.javacomponents/browser_ui/webshare/android/java/src/org/chromium/components/browser_ui/webshare/BlobReceiver.javachrome/android/java/src/org/chromium/chrome/browser/share/ShareDelegateImpl.javachrome/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
- Missing User Activation Check: The
blink.mojom.ShareServiceMojo interface is implemented inShareServiceImpl.javawithin 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. - Unvalidated Data Persistence: When the renderer calls the Mojo
Sharemethod with a file,ShareServiceImplvalidates 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 animage/pngwith a.pngextension).BlobReceiver.javathen 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. - Unsandboxed Decoding: To display the Sharing Hub bottom sheet,
ShareSheetBottomSheetContent.createPreview()is called to generate a thumbnail. For image MIME types, it invokesShareImageFileUtils.getBitmapFromUriAsync(), which delegates toApiCompatibilityUtils.getBitmapByUri(). On Android P and above, this callsandroid.graphics.ImageDecoder.decodeBitmap(). - 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.
- An attacker gains arbitrary code execution within the sandboxed renderer process.
- The compromised renderer connects to the
blink.mojom.ShareServiceinterface. - The attacker crafts a payload exploiting a known vulnerability in an Android native image codec (e.g., CVE-2023-4863 in
libwebp). - The payload is encapsulated in a
SharedFileobject, with its extension spoofed to.pngand MIME type spoofed toimage/pngto bypass initial validation inShareServiceImpl. - The renderer sends the
ShareIPC message. The lack of a user activation check permits this action to proceed silently. - The browser process saves the malformed file and attempts to generate a preview using
ImageDecoder.decodeBitmap(). - The Android system codec parses the malformed data in-process, triggering the vulnerability and granting the attacker RCE in the browser process.
Suggested Fix
- Enforce User Activation: Add a
HasTransientUserActivation()check in the browser process, either during the Mojo interface binding inForwardToJavaWebContentsor withinShareServiceImpl.share(), to ensure the sharing flow requires legitimate user interaction. - 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.