CVE-2026-106391
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ChromeInterfaceRegistrarchrome/android/java/src/org/chromium/chrome/browser/mojo/ChromeInterfaceRegistrar.java |
modified | |
ifchrome/android/java/src/org/chromium/chrome/browser/webshare/ShareServiceImplementationFactory.java |
modified |
Files Changed
chrome/browser/chrome_browser_interface_binders.ccchrome/android/java/src/org/chromium/chrome/browser/webshare/ShareServiceImplementationFactory.javachrome/android/junit/src/org/chromium/chrome/browser/share/ShareDelegateImplUnitTest.javachrome/android/java/src/org/chromium/chrome/browser/mojo/ChromeInterfaceRegistrar.java
Patch
From 5f926e7acdc0beff0548d7493c8b84622716b4bf Mon Sep 17 00:00:00 2001
From: Ari Chivukula <arichiv@chromium.org>
Date: Tue, 08 Sep 2026 00:27:09 -0700
Subject: [PATCH] Scope Android ShareService to RenderFrameHost lifecycle
On Android, ShareService was previously bound at the WebContents scope
via ForwardToJavaWebContents and ChromeWebContentsInterfaceRegistrar.
This caused the Mojo connection and permissions policy checks to
reference the top-level main frame rather than the specific requesting
document frame.
Fixed: 522325665
Change-Id: Iaeb7d7b9d98000d50bf4d1e967ffc3ca02d918e5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8368085
Auto-Submit: Ari Chivukula <arichiv@chromium.org>
Reviewed-by: Peter Conn <peconn@chromium.org>
Commit-Queue: Peter Conn <peconn@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1693593}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/mojo/ChromeInterfaceRegistrar.java b/chrome/android/java/src/org/chromium/chrome/browser/mojo/ChromeInterfaceRegistrar.java
index df71040a..b66b5e8ba 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/mojo/ChromeInterfaceRegistrar.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/mojo/ChromeInterfaceRegistrar.java
@@ -16,7 +16,6 @@
import org.chromium.chrome.browser.webshare.ShareServiceImplementationFactory;
import org.chromium.content_public.browser.InterfaceRegistrar;
import org.chromium.content_public.browser.RenderFrameHost;
-import org.chromium.content_public.browser.WebContents;
import org.chromium.installedapp.mojom.InstalledAppProvider;
import org.chromium.payments.mojom.DigitalGoodsFactory;
import org.chromium.payments.mojom.PaymentRequest;
@@ -28,23 +27,10 @@
class ChromeInterfaceRegistrar {
@CalledByNative
private static void registerMojoInterfaces() {
- InterfaceRegistrar.Registry.addWebContentsRegistrar(
- new ChromeWebContentsInterfaceRegistrar());
InterfaceRegistrar.Registry.addRenderFrameHostRegistrar(
new ChromeRenderFrameHostInterfaceRegistrar());
}
- private static class ChromeWebContentsInterfaceRegistrar
- implements InterfaceRegistrar<WebContents> {
- @Override
- public void registerInterfaces(
- InterfaceRegistry registry, @Nullable final WebContents webContents) {
- assert webContents != null;
- registry.addInterface(
- ShareService.MANAGER, new ShareServiceImplementationFactory(webContents));
- }
- }
-
private static class ChromeRenderFrameHostInterfaceRegistrar
implements InterfaceRegistrar<RenderFrameHost> {
@Override
@@ -59,6 +45,8 @@
Authenticator.MANAGER, new ChromeAuthenticatorFactory(renderFrameHost));
registry.addInterface(
DigitalGoodsFactory.MANAGER, new DigitalGoodsFactoryFactory(renderFrameHost));
+ registry.addInterface(
+ ShareService.MANAGER, new ShareServiceImplementationFactory(renderFrameHost));
}
}
}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/webshare/ShareServiceImplementationFactory.java b/chrome/android/java/src/org/chromium/chrome/browser/webshare/ShareServiceImplementationFactory.java
index d35cf16..0072595 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/webshare/ShareServiceImplementationFactory.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/webshare/ShareServiceImplementationFactory.java
@@ -4,7 +4,6 @@
package org.chromium.chrome.browser.webshare;
-
import org.chromium.build.annotations.NullMarked;
import org.chromium.build.annotations.Nullable;
import org.chromium.chrome.browser.share.ChromeShareExtras;
@@ -17,6 +16,7 @@
import org.chromium.content_public.browser.PermissionsPolicyFeature;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.WebContents;
+import org.chromium.content_public.browser.WebContentsStatics;
import org.chromium.services.service_manager.InterfaceFactory;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.webshare.mojom.ShareService;
@@ -26,15 +26,18 @@
/** Factory that creates instances of ShareService. */
@NullMarked
public class ShareServiceImplementationFactory implements InterfaceFactory<@Nullable ShareService> {
- private final WebContents mWebContents;
+ private final RenderFrameHost mRenderFrameHost;
private @Nullable Supplier<@Nullable ShareDelegate> mShareDelegateSupplier;
private @Nullable WindowAndroid mWindowAndroid;
- public ShareServiceImplementationFactory(WebContents webContents) {
- mWebContents = webContents;
- mWindowAndroid = mWebContents.getTopLevelNativeWindow();
+ public ShareServiceImplementationFactory(RenderFrameHost renderFrameHost) {
+ mRenderFrameHost = renderFrameHost;
+ WebContents webContents = WebContentsStatics.fromRenderFrameHost(mRenderFrameHost);
+ mWindowAndroid =
+ (webContents != null && !webContents.isDestroyed())
+ ? webContents.getTopLevelNativeWindow()
+ : null;
mShareDelegateSupplier = getShareDelegateSupplier(mWindowAndroid);
- assert mShareDelegateSupplier != null;
}
private static @Nullable Supplier<@Nullable ShareDelegate> getShareDelegateSupplier(
@@ -50,11 +53,12 @@
new ShareServiceImpl.WebShareDelegate() {
@Override
public boolean canShare() {
- if (mWebContents.isDestroyed()) return false;
+ WebContents webContents =
+ WebContentsStatics.fromRenderFrameHost(mRenderFrameHost);
+ if (webContents == null || webContents.isDestroyed()) return false;
return getShareDelegate() != null
- && mWebContents
- .getMainFrame()
- .isFeatureEnabled(PermissionsPolicyFeature.WEB_SHARE);
+ && mRenderFrameHost.isFeatureEnabled(
+ PermissionsPolicyFeature.WEB_SHARE);
}
@Override
@@ -65,26 +69,25 @@
params,
new ChromeShareExtras.Builder()
.setDetailedContentType(DetailedContentType.WEB_SHARE)
+ .setRenderFrameHost(mRenderFrameHost)
.build(),
ShareOrigin.WEBSHARE_API);
}
@Override
public @Nullable WindowAndroid getWindowAndroid() {
- if (mWebContents.isDestroyed()) return null;
+ WebContents webContents =
+ WebContentsStatics.fromRenderFrameHost(mRenderFrameHost);
+ if (webContents == null || webContents.isDestroyed()) return null;
if (mWindowAndroid == null || mWindowAndroid.isDestroyed()) {
- mWindowAndroid = mWebContents.getTopLevelNativeWindow();
+ mWindowAndroid = webContents.getTopLevelNativeWindow();
}
return mWindowAndroid;
}
@Override
public void terminateRendererDueToBadMessage(int reason) {
- if (mWebContents.isDestroyed()) return;
- RenderFrameHost mainFrame = mWebContents.getMainFrame();
- if (mainFrame != null) {
- mainFrame.terminateRendererDueToBadMessage(reason);
- }
+ mRenderFrameHost.terminateRendererDueToBadMessage(reason);
}
/**
@@ -95,16 +98,18 @@
* necessitates getting a new ShareDelegate. See https://crbug.com/40838216.
*/
private @Nullable ShareDelegate getShareDelegate() {
- if (mWebContents.isDestroyed()) return null;
+ WebContents webContents =
+ WebContentsStatics.fromRenderFrameHost(mRenderFrameHost);
+ if (webContents == null || webContents.isDestroyed()) return null;
+ WindowAndroid currentWindow = webContents.getTopLevelNativeWindow();
if (mWindowAndroid != null
- && mWindowAndroid.equals(mWebContents.getTopLevelNativeWindow())
+ && mWindowAndroid.equals(currentWindow)
&& mShareDelegateSupplier != null) {
return mShareDelegateSupplier.get();
}
- mWindowAndroid = getWindowAndroid();
+ mWindowAndroid = currentWindow;
mShareDelegateSupplier = getShareDelegateSupplier(mWindowAndroid);
- assert mShareDelegateSupplier != null;
- return mShareDelegateSupplier.get();
+ return mShareDelegateSupplier != null ? mShareDelegateSupplier.get() : null;
}
};
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/share/ShareDelegateImplUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/share/ShareDelegateImplUnitTest.java
index a42dd78f..f9f4094 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/share/ShareDelegateImplUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/share/ShareDelegateImplUnitTest.java
@@ -9,6 +9,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -58,6 +59,7 @@
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.browser.ui.signin.SigninAndHistorySyncActivityLauncher;
+import org.chromium.chrome.browser.webshare.ShareServiceImplementationFactory;
import org.chromium.chrome.test.OverrideContextWrapperTestRule;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.share.ShareParams;
@@ -66,12 +68,17 @@
import org.chromium.components.dom_distiller.core.DomDistillerUrlUtilsJni;
import org.chromium.components.favicon.LargeIconBridgeJni;
import org.chromium.components.feature_engagement.Tracker;
+import org.chromium.content_public.browser.PermissionsPolicyFeature;
import org.chromium.content_public.browser.RenderFrameHost;
+import org.chromium.content_public.browser.WebContents;
+import org.chromium.content_public.browser.WebContentsStatics;
import org.chromium.ui.base.ActivityResultTracker;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.url.GURL;
import org.chromium.url.JUnitTestGURLs;
+import org.chromium.webshare.mojom.ShareError;
+import org.chromium.webshare.mojom.ShareService;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@@ -440,10 +447,200 @@
Assert.assertEquals(shareText, mShareParamsCaptor.getValue().getText());
}
+ @Test
+ public void testWebShareText_allowedByPolicy() {
+ doAnswer(sShareIsAllowedByPolicy)
+ .when(mDataProtectionBridgeMock)
+ .verifyShareTextIsAllowedByPolicy(anyString(), any(), any());
+ String shareText = "sample_data";
+
+ ShareParams shareParams =
+ new ShareParams.Builder(mWindowAndroid, "sample_title", "")
+ .setText(shareText)
+ .build();
+ ChromeShareExtras chromeShareExtras =
+ new ChromeShareExtras.Builder()
+ .setDetailedContentType(DetailedContentType.WEB_SHARE)
+ .setRenderFrameHost(mRenderFrameHost)
+ .build();
+
+ testShareExpectAllowed(shareParams, chromeShareExtras, ShareOrigin.WEBSHARE_API);
+ Assert.assertEquals(shareText, mShareParamsCaptor.getValue().getText());
+ }
+
+ @Test
+ public void testWebShareText_notAllowedByPolicy() {
+ doAnswer(sShareIsNotAllowedByPolicy)
+ .when(mDataProtectionBridgeMock)
+ .verifyShareTextIsAllowedByPolicy(anyString(), any(), any());
+ String shareText = "sample_data";
+
+ ShareParams shareParams =
+ new ShareParams.Builder(mWindowAndroid, "sample_title", "")
+ .setText(shareText)
+ .build();
+ ChromeShareExtras chromeShareExtras =
+ new ChromeShareExtras.Builder()
+ .setDetailedContentType(DetailedContentType.WEB_SHARE)
+ .setRenderFrameHost(mRenderFrameHost)
+ .build();
+
+ testShareExpectNotAllowed(shareParams, chromeShareExtras, ShareOrigin.WEBSHARE_API);
+ }
+
+ @Test
+ public void testWebShareLink_allowedByPolicy() {
+ doAnswer(sShareIsAllowedByPolicy)
+ .when(mDataProtectionBridgeMock)
+ .verifyShareUrlIsAllowedByPolicy(anyString(), any(), any());
+ String shareUrl = "https://example.com";
+
+ ShareParams shareParams =
+ new ShareParams.Builder(mWindowAndroid, "sample_title", shareUrl)
+ .setBypassFixingDomDistillerUrl(true)
+ .build();
+ ChromeShareExtras chromeShareExtras =
+ new ChromeShareExtras.Builder()
+ .setDetailedContentType(DetailedContentType.WEB_SHARE)
+ .setRenderFrameHost(mRenderFrameHost)
+ .build();
+
+ testShareExpectAllowed(shareParams, chromeShareExtras, ShareOrigin.WEBSHARE_API);
+ Assert.assertEquals(shareUrl, mShareParamsCaptor.getValue().getUrl());
+ }
+
+ @Test
+ public void testWebShareLink_notAllowedByPolicy() {
+ doAnswer(sShareIsNotAllowedByPolicy)
+ .when(mDataProtectionBridgeMock)
+ .verifyShareUrlIsAllowedByPolicy(anyString(), any(), any());
+ String shareUrl = "https://example.com";
+
+ ShareParams shareParams =
+ new ShareParams.Builder(mWindowAndroid, "sample_title", shareUrl)
+ .setBypassFixingDomDistillerUrl(true)
+ .build();
+ ChromeShareExtras chromeShareExtras =
+ new ChromeShareExtras.Builder()
+ .setDetailedContentType(DetailedContentType.WEB_SHARE)
+ .setRenderFrameHost(mRenderFrameHost)
+ .build();
+
+ testShareExpectNotAllowed(shareParams, chromeShareExtras, ShareOrigin.WEBSHARE_API);
+ }
+
+ @Test
+ public void testShareServiceImplementationFactory_frameScopingAndDelegation() {
+ WebContents webContents = mock(WebContents.class);
+ doReturn(mWindowAndroid).when(webContents).getTopLevelNativeWindow();
+ WebContentsStatics.setWebContentsForTesting(webContents);
+
+ ShareDelegate mockShareDelegate = mock(ShareDelegate.class);
+ ShareDelegateSupplier.setInstanceForTesting(
+ ObservableSuppliers.createMonotonic(mockShareDelegate));
+
+ doReturn(true).when(mRenderFrameHost).isFeatureEnabled(PermissionsPolicyFeature.WEB_SHARE);
+
+ ShareServiceImplementationFactory factory =
+ new ShareServiceImplementationFactory(mRenderFrameHost);
+ ShareService shareService = factory.createImpl();
+ Assert.assertNotNull(shareService);
+
+ org.chromium.url.mojom.Url invalidUrl = new org.chromium.url.mojom.Url();
+ invalidUrl.url = "javascript:void(0)";
+
+ int[] shareError = new int[1];
+ shareService.share(
+ "sample_title", "sample_text", invalidUrl, null, error -> shareError[0] = error);
+
+ Assert.assertEquals(ShareError.PERMISSION_DENIED, shareError[0]);
+ verify(mRenderFrameHost).terminateRendererDueToBadMessage(11);
+
+ org.chromium.url.mojom.Url validUrl = new org.chromium.url.mojom.Url();
+ validUrl.url = "https://example.com";
+
+ shareService.share("sample_title", "sample_text", validUrl, null, error -> {});
+
+ ArgumentCaptor<ChromeShareExtras> extrasCaptor =
+ ArgumentCaptor.forClass(ChromeShareExtras.class);
+ verify(mockShareDelegate)
+ .share(
+ any(ShareParams.class),
+ extrasCaptor.capture(),
+ eq(ShareOrigin.WEBSHARE_API));
+
+ ChromeShareExtras capturedExtras = extrasCaptor.getValue();
+ Assert.assertEquals(mRenderFrameHost, capturedExtras.getRenderFrameHost());
+ Assert.assertEquals(DetailedContentType.WEB_SHARE, capturedExtras.getDetailedContentType());
+ }
+
+ @Test
+ public void testShareServiceImplementationFactory_permissionsPolicyDisabled() {
+ WebContents webContents = mock(WebContents.class);
+ doReturn(mWindowAndroid).when(webContents).getTopLevelNativeWindow();
+ WebContentsStatics.setWebContentsForTesting(webContents);
+
+ ShareDelegate mockShareDelegate = mock(ShareDelegate.class);
+ ShareDelegateSupplier.setInstanceForTesting(
+ ObservableSuppliers.createMonotonic(mockShareDelegate));
+
+ doReturn(false).when(mRenderFrameHost).isFeatureEnabled(PermissionsPolicyFeature.WEB_SHARE);
+
+ ShareServiceImplementationFactory factory =
+ new ShareServiceImplementationFactory(mRenderFrameHost);
+ ShareService shareService = factory.createImpl();
+ Assert.assertNotNull(shareService);
+
+ org.chromium.url.mojom.Url validUrl = new org.chromium.url.mojom.Url();
+ validUrl.url = "https://example.com";
+
+ int[] shareError = new int[1];
+ shareService.share(
+ "sample_title", "sample_text", validUrl, null, error -> shareError[0] = error);
+
+ Assert.assertEquals(ShareError.INTERNAL_ERROR, shareError[0]);
+ verify(mockShareDelegate, never()).share(any(), any(), anyInt());
+ }
+
+ @Test
+ public void testShareServiceImplementationFactory_destroyedWebContents() {
+ WebContents webContents = mock(WebContents.class);
+ doReturn(true).when(webContents).isDestroyed();
+ WebContentsStatics.setWebContentsForTesting(webContents);
+
+ ShareDelegate mockShareDelegate = mock(ShareDelegate.class);
+ ShareDelegateSupplier.setInstanceForTesting(
+ ObservableSuppliers.createMonotonic(mockShareDelegate));
+
+ doReturn(true).when(mRenderFrameHost).isFeatureEnabled(PermissionsPolicyFeature.WEB_SHARE);
+
+ ShareServiceImplementationFactory factory =
+ new ShareServiceImplementationFactory(mRenderFrameHost);
+ ShareService shareService = factory.createImpl();
+ Assert.assertNotNull(shareService);
+
+ org.chromium.url.mojom.Url validUrl = new org.chromium.url.mojom.Url();
+ validUrl.url = "https://example.com";
+
+ int[] shareError = new int[1];
... (file diff truncated)
Original Bug Report
ShareService Scope Issue leads to UI Spoofing and Renderer DoS
Flapjack, 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: The ShareService Mojo interface on Android is registered at the WebContents scope rather than the RenderFrameHost scope. This allows a compromised renderer to retain the Mojo pipe after cross-origin navigation. By abusing this orphaned connection, an attacker can potentially trigger cross-origin renderer termination, bypass DLP policies, and spoof the native Share Sheet UI.
Affected files:
chrome/android/java/src/org/chromium/chrome/browser/mojo/ChromeInterfaceRegistrar.javachrome/android/java/src/org/chromium/chrome/browser/webshare/ShareServiceImplementationFactory.javachrome/android/java/src/org/chromium/chrome/browser/share/ShareDelegateImpl.java
Estimated timestamp from git blame: 2016-09-28
Summary
On Android, the ShareService Mojo interface is improperly scoped to the lifecycle of the WebContents rather than the specific RenderFrameHost. This architectural mismatch allows a compromised renderer to maintain a functional Mojo connection to the browser process even after the tab has navigated to a different, cross-origin site. When the attacker invokes this orphaned remote, the browser incorrectly processes the request using the state of the new origin (via mWebContents.getMainFrame()), leading to severe security consequences.
Potential Exploit Scenario
Note: The following steps are a potential exploit scenario based on code analysis; a working proof of concept has not yet been executed.
- Compromise: An attacker exploits a vulnerability to compromise a renderer process for a site they control (e.g.,
attacker.com). - Bind Interface: The compromised renderer requests to bind the
blink::mojom::ShareServiceinterface. InChromeInterfaceRegistrar.java, this is handled byChromeWebContentsInterfaceRegistrar, tying the interface to theWebContentsviaShareServiceImplementationFactory. - Cross-Origin Navigation: The attacker triggers a top-level navigation within the same tab to a highly trusted target site (e.g.,
bank.com). - Mojo Pipe Survival: Because the
ShareServiceis scoped to theWebContents, the Mojo connection is not torn down during the navigation. The attacker’s renderer retains the active remote.
Impacts
By invoking the retained ShareService remote while the user is viewing the target site, the attacker can achieve three distinct impacts:
1. Cross-Process Renderer Termination (Site Isolation Violation)
If the attacker sends a share() IPC with a malformed URL (e.g., invalid scheme), ShareServiceImpl.share() calls mDelegate.terminateRendererDueToBadMessage(). The delegate handles this by calling mWebContents.getMainFrame().terminateRendererDueToBadMessage(reason). Because the main frame now points to the target site (bank.com), the browser incorrectly kills the innocent cross-origin renderer process, leaving the attacker’s renderer alive.
2. Enterprise Policy Bypass
If the attacker sends a valid share() IPC, ShareServiceImplementationFactory creates a ChromeShareExtras object without setting a RenderFrameHost. When this reaches ShareDelegateImpl.shareIfAllowedByPolicy(), the missing RenderFrameHost causes an early return (shareCallback.onResult(true)), bypassing any configured Data Leak Prevention (DLP) enterprise policies.
3. UI Spoofing
The native Android Share Sheet is presented to the user. Because the action is executed in the context of the current WebContents, system-level attribution and permission checks (such as isFeatureEnabled(PermissionsPolicyFeature.WEB_SHARE)) are evaluated against the currently visible site (bank.com), while the shared content is entirely attacker-controlled.
Suggested Fix
- Correct Scoping: In
ChromeInterfaceRegistrar.java, move the registration ofShareService.MANAGERfromChromeWebContentsInterfaceRegistrartoChromeRenderFrameHostInterfaceRegistrar. - Bind RenderFrameHost: Update
ShareServiceImplementationFactoryto take aRenderFrameHostinstead of aWebContentsin its constructor. - Use Bound Origin: Update the
ShareServiceImpl.WebShareDelegateimplementation to use the boundRenderFrameHostfor policy checks (isFeatureEnabled), bad message termination, and explicitly set it when buildingChromeShareExtrasto ensure DLP checks function correctly.
Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff
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.