Medium chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in CustomTabs
DescriptionMemory corruption in CustomTabs
ComponentCustomTabs
Sub-componentCCT
Bug ClassMemory Corruption
Tracker513789297
Fix commit963b0290da01 (chromium/src) +128/-141
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
chrome/android/java/src/org/chromium/chrome/browser/browserservices/PostMessageHandler.java
modified

Files Changed

  • chrome/android/junit/src/org/chromium/chrome/browser/browserservices/PostMessageHandlerTest.java
  • content/public/android/java/src/org/chromium/content_public/browser/MessagePort.java
  • android_webview/glue/java/src/com/android/webview/chromium/WebMessagePortAdapter.java
  • android_webview/javatests/src/org/chromium/android_webview/test/PostMessageTest.java
  • content/browser/android/app_web_message_port.cc
  • android_webview/support_library/java/src/org/chromium/support_lib_glue/SupportLibWebMessagePortAdapter.java
  • content/public/android/java/src/org/chromium/content/browser/AppWebMessagePort.java
  • chrome/android/java/src/org/chromium/chrome/browser/browserservices/PostMessageHandler.java
From 963b0290da0163bb9befec64a1c3abe0efa9c098 Mon Sep 17 00:00:00 2001
From: Jinsuk Kim <jinsukkim@google.com>
Date: Fri, 18 Sep 2026 14:41:59 -0700
Subject: [PATCH] [CCT] Attribute PostMessage origin to sender origin

Previously, Chrome Custom Tabs (CCT) PostMessageHandler attributed the
origin of incoming MessagePort messages to the top-level main frame's
committed URL. If a MessagePort was transferred to an embedded
cross-origin iframe, messages sent from that cross-origin context were
still reported to the host Android application with the main frame's
origin in the androidx.browser.customtabs.POST_MESSAGE_ORIGIN bundle
extra, allowing cross-origin frames to spoof trusted origins.

This CL plumbs the actual message sender's origin from Blink's
TransferableMessage through to Java:

1. Native JNI Layer (content/browser/android/app_web_message_port.cc):
   - Extracts |sender_origin| from |blink::TransferableMessage|,
     serializes it via |url::Origin::Serialize()|, and passes it to
     Java in |Java_AppWebMessagePort_onMessage|.
2. Java MessagePort Framework:
   - Updates |AppWebMessagePort.onMessage| and |MessageHandler| to carry
     |@Nullable String senderOrigin| in a typed |MessagePortMessage|
     object.
   - Updates |MessagePort.MessageCallback#onMessage| signature to
     receive |@Nullable String senderOrigin|.
   - Updates WebView adapters (|WebMessagePortAdapter|,
     |SupportLibWebMessagePortAdapter|) and |PostMessageTest| call
     sites.
3. CCT PostMessageHandler:
   - Populates |POST_MESSAGE_ORIGIN| directly using the message's
     |senderOrigin| instead of querying the main frame's URL.
   - Adds unit test coverage in |PostMessageHandlerTest| for same-
     origin, cross-origin, null, opaque, and empty sender origins.

TAG=agy
CONV=13f95f2b-7c7d-4300-a5d8-1bb98f45a5ef

Bug: 513789297
Test: PostMessageHandlerTest,PostMessageTest
Change-Id: Idff299e8daa56ac55882b6e57102b9ab4579eb89
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8417627
Reviewed-by: Richard Coles <torne@chromium.org>
Reviewed-by: Daniel Murphy <dmurph@chromium.org>
Commit-Queue: Jinsuk Kim <jinsukkim@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1701097}
---
diff --git a/android_webview/glue/java/src/com/android/webview/chromium/WebMessagePortAdapter.java b/android_webview/glue/java/src/com/android/webview/chromium/WebMessagePortAdapter.java
index ddaed599..7515e64 100644
--- a/android_webview/glue/java/src/com/android/webview/chromium/WebMessagePortAdapter.java
+++ b/android_webview/glue/java/src/com/android/webview/chromium/WebMessagePortAdapter.java
@@ -41,14 +41,10 @@
     @Override
     public void setWebMessageCallback(final WebMessageCallback callback, final Handler handler) {
         mPort.setMessageCallback(
-                new MessagePort.MessageCallback() {
-                    @Override
-                    public void onMessage(MessagePayload messagePayload, MessagePort[] ports) {
-                        callback.onMessage(
-                                WebMessagePortAdapter.this,
-                                new WebMessage(
-                                        messagePayload.getAsString(), fromMessagePorts(ports)));
-                    }
+                (messagePayload, ports, senderOrigin) -> {
+                    callback.onMessage(
+                            WebMessagePortAdapter.this,
+                            new WebMessage(messagePayload.getAsString(), fromMessagePorts(ports)));
                 },
                 handler);
     }
diff --git a/android_webview/support_library/java/src/org/chromium/support_lib_glue/SupportLibWebMessagePortAdapter.java b/android_webview/support_library/java/src/org/chromium/support_lib_glue/SupportLibWebMessagePortAdapter.java
index fea59acf..c1169362b 100644
--- a/android_webview/support_library/java/src/org/chromium/support_lib_glue/SupportLibWebMessagePortAdapter.java
+++ b/android_webview/support_library/java/src/org/chromium/support_lib_glue/SupportLibWebMessagePortAdapter.java
@@ -10,7 +10,6 @@
 
 import org.chromium.android_webview.common.Lifetime;
 import org.chromium.base.TraceEvent;
-import org.chromium.content_public.browser.MessagePayload;
 import org.chromium.content_public.browser.MessagePort;
 import org.chromium.support_lib_boundary.WebMessageBoundaryInterface;
 import org.chromium.support_lib_boundary.WebMessageCallbackBoundaryInterface;
@@ -83,13 +82,10 @@
                         BoundaryInterfaceReflectionUtil.castToSuppLibClass(
                                 WebMessageCallbackBoundaryInterface.class, callback));
         mPort.setMessageCallback(
-                new MessagePort.MessageCallback() {
-                    @Override
-                    public void onMessage(MessagePayload messagePayload, MessagePort[] ports) {
-                        callbackAdapter.onMessage(
-                                SupportLibWebMessagePortAdapter.this,
-                                new SupportLibWebMessageAdapter(messagePayload, ports));
-                    }
+                (messagePayload, ports, senderOrigin) -> {
+                    callbackAdapter.onMessage(
+                            SupportLibWebMessagePortAdapter.this,
+                            new SupportLibWebMessageAdapter(messagePayload, ports));
                 },
                 handler);
     }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/PostMessageHandler.java b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/PostMessageHandler.java
index 8a451d6..87744ba 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/browserservices/PostMessageHandler.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/browserservices/PostMessageHandler.java
@@ -35,7 +35,6 @@
 import org.chromium.content_public.browser.RenderFrameHost;
 import org.chromium.content_public.browser.WebContents;
 import org.chromium.content_public.browser.WebContentsObserver;
-import org.chromium.net.GURLUtils;
 import org.chromium.url.GURL;
 
 /**
@@ -46,8 +45,7 @@
     private static final String TAG = "PostMessageHandler";
 
     // TODO(crbug.com/40257514): This should get moved into androidx.browser.
-    private static final String POST_MESSAGE_ORIGIN =
-            "androidx.browser.customtabs.POST_MESSAGE_ORIGIN";
+    static final String POST_MESSAGE_ORIGIN = "androidx.browser.customtabs.POST_MESSAGE_ORIGIN";
 
     private final MessageCallback mMessageCallback;
     private final PostMessageBackend mPostMessageBackend;
@@ -55,13 +53,6 @@
     private @Nullable WebContentsObserver mWebContentsObserver;
     private MessagePort @Nullable [] mChannel;
 
-    /**
-     * The origin of the document {@link #mChannel} was created for, captured at channel creation
-     * time. Resolving the origin lazily per message would be racy: a message can sit in the UI task
-     * queue while a navigation commits, which would attribute it to the wrong document.
-     */
-    private @Nullable String mChannelOrigin;
-
     private @Nullable Uri mPostMessageSourceUri;
     private @Nullable Uri mPostMessageTargetUri;
 
@@ -77,7 +68,7 @@
     public PostMessageHandler(PostMessageBackend postMessageBackend) {
         mPostMessageBackend = postMessageBackend;
         mMessageCallback =
-                (messagePayload, sentPorts) -> {
+                (messagePayload, sentPorts, senderOrigin) -> {
                     if (mChannel == null) {
                         Log.e(TAG, "Discarding postMessage as channel is null.");
                         return;
@@ -94,9 +85,13 @@
                     }
 
                     Bundle bundle = null;
-                    if (mChannelOrigin != null && !mChannelOrigin.isEmpty()) {
+                    // For opaque origins (e.g., sandboxed iframes without allow-same-origin or
+                    // data: URLs), senderOrigin is serialized as the literal string "null" per
+                    // RFC 6454 and forwarded in POST_MESSAGE_ORIGIN to align with the web
+                    // platform's MessageEvent.origin.
+                    if (senderOrigin != null && !senderOrigin.isEmpty()) {
                         bundle = new Bundle();
-                        bundle.putString(POST_MESSAGE_ORIGIN, mChannelOrigin);
+                        bundle.putString(POST_MESSAGE_ORIGIN, senderOrigin);
                     }
                     assumeNonNull(messagePayload.getAsString());
                     mPostMessageBackend.onPostMessage(messagePayload.getAsString(), bundle);
@@ -213,7 +208,6 @@
         closeChannelPort();
         mChannel = webContents.createMessageChannel();
         mChannel[0].setMessageCallback(mMessageCallback, null);
-        mChannelOrigin = getCommittedOrigin(webContents);
 
         assumeNonNull(mPostMessageSourceUri);
         webContents.postMessageToMainFrame(
@@ -226,19 +220,6 @@
     }
 
     /**
-     * @return The origin of the primary main frame's committed document, in the same format
-     *     previously reported to clients via {@link #POST_MESSAGE_ORIGIN}, or null if it cannot be
-     *     determined.
-     */
-    private static @Nullable String getCommittedOrigin(WebContents webContents) {
-        RenderFrameHost mainFrame = webContents.getMainFrame();
-        if (mainFrame == null) return null;
-        GURL url = mainFrame.getLastCommittedURL();
-        if (url == null || url.isEmpty() || !url.isValid()) return null;
-        return GURLUtils.getOrigin(url.getSpec());
-    }
-
-    /**
      * Drops the message channel without notifying the client. {@link MessagePort#close()} throws if
      * the port has already been transferred, so guard against that.
      */
@@ -248,7 +229,6 @@
             mChannel[0].close();
         }
         mChannel = null;
-        mChannelOrigin = null;
     }
 
     /**
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/browserservices/PostMessageHandlerTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/browserservices/PostMessageHandlerTest.java
index 53c1ddd7..5d4b6f17 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/browserservices/PostMessageHandlerTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/browserservices/PostMessageHandlerTest.java
@@ -22,7 +22,6 @@
 import androidx.browser.customtabs.CustomTabsService;
 import androidx.browser.customtabs.PostMessageBackend;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -49,8 +48,6 @@
 import org.chromium.content_public.browser.WebContents;
 import org.chromium.content_public.browser.WebContentsObserver;
 import org.chromium.mojo_base.mojom.UnguessableToken;
-import org.chromium.net.GURLUtils;
-import org.chromium.net.GURLUtilsJni;
 import org.chromium.url.GURL;
 
 /** Unit tests for {@link PostMessageHandler}. */
@@ -78,7 +75,6 @@
     @Mock private MessagePort mSecondLocalPort;
     @Mock private MessagePort mSecondRemotePort;
     @Captor private ArgumentCaptor<WebContentsObserver> mObserverCaptor;
-    @Mock private GURLUtils.Natives mGURLUtilsJni;
 
     private PostMessageHandler mHandler;
 
@@ -98,16 +94,9 @@
                 .thenReturn(new MessagePort[] {mSecondLocalPort, mSecondRemotePort});
         lenient().when(mNavigation.hasCommitted()).thenReturn(true);
         lenient().when(mNavigation.isSameDocument()).thenReturn(false);
-        GURLUtilsJni.setInstanceForTesting(mGURLUtilsJni);
-        lenient().when(mGURLUtilsJni.getOrigin(any())).thenReturn("https://www.example.com");
         mHandler = new PostMessageHandler(mPostMessageBackend);
     }
 
-    @After
-    public void tearDown() {
-        GURLUtilsJni.setInstanceForTesting(null);
-    }
-
     @Test
     public void testReinitializeChannelAfterMainFrameNavigation() {
         WebContentsObserver observer = resetAndFinishInitialNavigation();
@@ -410,7 +399,7 @@
     public void testMessageCallbackSendsOriginInBundle() {
         MessageCallback callback = initializeAndCaptureMessageCallback();
 
-        callback.onMessage(new MessagePayload("test_message"), null);
+        callback.onMessage(new MessagePayload("test_message"), null, "https://www.example.com");
 
         ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
         verify(mPostMessageBackend).onPostMessage(eq("test_message"), bundleCaptor.capture());
@@ -420,74 +409,47 @@
     }
 
     @Test
-    public void testMessageCallbackUsesOriginCapturedAtChannelCreation() {
+    public void testMessageCallbackWithCrossOriginSenderOrigin() {
         MessageCallback callback = initializeAndCaptureMessageCallback();
 
-        // The document navigates away after the channel was created. A message already queued on
-        // the UI task runner must still be attributed to the document that sent it, not to
-        // whatever happens to be committed by the time it is delivered. These stubs are lenient
-        // precisely because the handler must no longer consult them.
-        lenient()
-                .when(mMainFrame.getLastCommittedURL())
-                .thenReturn(new GURL("https://other.example.com/p"));
-        lenient().when(mGURLUtilsJni.getOrigin(any())).thenReturn("https://other.example.com");
-
-        callback.onMessage(new MessagePayload("test_message"), null);
+        callback.onMessage(new MessagePayload("test_message"), null, "https://evil.example.com");
 
         ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
         verify(mPostMessageBackend).onPostMessage(eq("test_message"), bundleCaptor.capture());
-        assertEquals(
-                "https://www.example.com", bundleCaptor.getValue().getString(POST_MESSAGE_ORIGIN));
+        Bundle bundle = bundleCaptor.getValue();
+        assertNotNull(bundle);
+        assertEquals("https://evil.example.com", bundle.getString(POST_MESSAGE_ORIGIN));
     }
 
     @Test
-    public void testMessageCallbackWithEmptyOriginSendsNullBundle() {
-        when(mGURLUtilsJni.getOrigin(any())).thenReturn("");
+    public void testMessageCallbackWithNullSenderOriginSendsNullBundle() {
         MessageCallback callback = initializeAndCaptureMessageCallback();
 
-        callback.onMessage(new MessagePayload("test_message"), null);
+        callback.onMessage(new MessagePayload("test_message"), null, null);
 
         verify(mPostMessageBackend).onPostMessage("test_message", null);
     }
 
     @Test
-    public void testMessageCallbackWithNullMainFrameUrlSendsNullBundle() {
-        when(mMainFrame.getLastCommittedURL()).thenReturn(null);
-        MessageCallback callback = initializeAndCaptureMessageCallback(/* targetUri= */ null);
+    public void testMessageCallbackWithEmptySenderOriginSendsNullBundle() {
+        MessageCallback callback = initializeAndCaptureMessageCallback();
 
-        callback.onMessage(new MessagePayload("test_message"), null);
+        callback.onMessage(new MessagePayload("test_message"), null, "");
 
         verify(mPostMessageBackend).onPostMessage("test_message", null);
     }
 
     @Test
-    public void testMessageCallbackWithEmptyMainFrameUrlSendsNullBundle() {
-        when(mMainFrame.getLastCommittedURL()).thenReturn(GURL.emptyGURL());
-        MessageCallback callback = initializeAndCaptureMessageCallback(/* targetUri= */ null);
+    public void testMessageCallbackWithOpaqueSenderOrigin() {
+        MessageCallback callback = initializeAndCaptureMessageCallback();
 
-        callback.onMessage(new MessagePayload("test_message"), null);
+        callback.onMessage(new MessagePayload("test_message"), null, "null");
 
-        verify(mPostMessageBackend).onPostMessage("test_message", null);
-    }
-
-    @Test
-    public void testMessageCallbackWithInvalidMainFrameUrlSendsNullBundle() {
-        when(mMainFrame.getLastCommittedURL()).thenReturn(new GURL("invalid url"));
-        MessageCallback callback = initializeAndCaptureMessageCallback(/* targetUri= */ null);
-
-        callback.onMessage(new MessagePayload("test_message"), null);
-
-        verify(mPostMessageBackend).onPostMessage("test_message", null);
-    }
-
-    @Test
-    public void testMessageCallbackWithNullMainFrameSendsNullBundle() {
-        when(mWebContents.getMainFrame()).thenReturn(null);
-        MessageCallback callback = initializeAndCaptureMessageCallback(/* targetUri= */ null);
-
-        callback.onMessage(new MessagePayload("test_message"), null);
-
-        verify(mPostMessageBackend).onPostMessage("test_message", null);
+        ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(mPostMessageBackend).onPostMessage(eq("test_message"), bundleCaptor.capture());
+        Bundle bundle = bundleCaptor.getValue();
+        assertNotNull(bundle);
+        assertEquals("null", bundle.getString(POST_MESSAGE_ORIGIN));
     }
 
     @Test
@@ -495,7 +457,7 @@
         MessageCallback callback = initializeAndCaptureMessageCallback();
         when(mFirstLocalPort.isTransferred()).thenReturn(true);
 
-        callback.onMessage(new MessagePayload("test_message"), null);
+        callback.onMessage(new MessagePayload("test_message"), null, "https://www.example.com");
 
         verify(mPostMessageBackend, never()).onPostMessage(any(), any());
     }
@@ -505,7 +467,7 @@
         MessageCallback callback = initializeAndCaptureMessageCallback();
         when(mWebContents.isDestroyed()).thenReturn(true);
 
-        callback.onMessage(new MessagePayload("test_message"), null);
+        callback.onMessage(new MessagePayload("test_message"), null, "https://www.example.com");
 
         verify(mPostMessageBackend, never()).onPostMessage(any(), any());
     }
@@ -515,7 +477,7 @@
         MessageCallback callback = initializeAndCaptureMessageCallback();
         mHandler.reset(null);
 
-        callback.onMessage(new MessagePayload("test_message"), null);
+        callback.onMessage(new MessagePayload("test_message"), null, "https://www.example.com");
 
         verify(mPostMessageBackend, never()).onPostMessage(any(), any());
     }
diff --git a/content/browser/android/app_web_message_port.cc b/content/browser/android/app_web_message_port.cc
index 9b110a2..2a03d6b0 100644
--- a/content/browser/android/app_web_message_port.cc
+++ b/content/browser/android/app_web_message_port.cc
@@ -5,6 +5,8 @@
 #include "content/browser/android/app_web_message_port.h"
 
 #include <memory>
+#include <optional>
+#include <string>
 
 #include "base/android/jni_android.h"
 #include "base/android/jni_array.h"
@@ -26,6 +28,7 @@
 #include "third_party/blink/public/common/messaging/web_message_port.h"
 #include "third_party/blink/public/mojom/blob/blob.mojom.h"
 #include "third_party/blink/public/mojom/messaging/transferable_message.mojom.h"
+#include "url/origin.h"
 
 // Must come after all headers that specialize FromJniType() / ToJniType().
 #include "content/public/android/content_jni_headers/AppWebMessagePort_jni.h"
@@ -164,6 +167,8 @@
     // Decode mojo message failed.
     return false;
   }
+  std::optional<url::Origin> sender_origin =
+      std::move(transferable_message.sender_origin);
   auto ports = std::move(transferable_message.ports);
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/PostMessageTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/PostMessageTest.java
index b7eec79..2646de1 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/PostMessageTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/PostMessageTest.java
@@ -40,9 +40,9 @@
 
 import java.nio.charset.StandardCharsets;
 import java.util.List;
-import java.util.Set;
 import java.util.Locale;
 import java.util.Random;
+import java.util.Set;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicReference;
@@ -96,10 +96,12 @@
         public static class Data {
             public final MessagePayload mMessagePayload;
             public final Looper mLastLooper;
+            public final String mSenderOrigin;
 
-            public Data(MessagePayload messagePayload, Looper looper) {
+            public Data(MessagePayload messagePayload, Looper looper, String senderOrigin) {
                 mMessagePayload = messagePayload;
                 mLastLooper = looper;
+                mSenderOrigin = senderOrigin;
             }
 
             public String getStringValue() {
@@ -120,8 +122,13 @@
         }
 
         public void notifyCalled(MessagePayload messagePayload) {
+            notifyCalled(messagePayload, null);
+        }
+
+        public void notifyCalled(MessagePayload messagePayload, String senderOrigin) {
             try {
-                mQueue.add(new ChannelContainer.Data(messagePayload, Looper.myLooper()));
+                mQueue.add(
+                        new ChannelContainer.Data(messagePayload, Looper.myLooper(), senderOrigin));
             } catch (IllegalStateException e) {
                 // We expect this add operation will always succeed since the default capacity of
                 // the queue is Integer.MAX_VALUE.
@@ -468,7 +475,7 @@
                 () -> {
                     MessagePort[] channel = mAwContents.createMessageChannel();
                     // set a web event handler, this puts the port in a started state.
-                    channel[1].setMessageCallback((_, _) -> {}, null);
+                    channel[1].setMessageCallback((_, _, _) -> {}, null);
                     Assert.assertThrows(
                             IllegalStateException.class,
                             () ->
@@ -509,7 +516,7 @@
                 () -> {
                     MessagePort[] channel1 = mAwContents.createMessageChannel();
                     // set a web event handler, this puts the port in a started state.
-                    channel1[1].setMessageCallback((_, _) -> {}, null);
+                    channel1[1].setMessageCallback((_, _, _) -> {}, null);
                     MessagePort[] channel2 = mAwContents.createMessageChannel();
                     Assert.assertThrows(
                             IllegalStateException.class,
@@ -738,7 +745,9 @@
         ThreadUtils.runOnUiThreadBlocking(
                 () -> {
                     channel[0].setMessageCallback(
-                            (message, _) -> channelContainer.notifyCalled(message), null);
+                            (message, _, senderOrigin) ->
+                                    channelContainer.notifyCalled(message, senderOrigin),
+                            null);
                     mAwContents.postMessageToMainFrame(
                             new MessagePayload(WEBVIEW_MESSAGE),
                             mWebServer.getBaseUrl(),
@@ -748,6 +757,10 @@
         // wait for the asynchronous response from JS
         ChannelContainer.Data data = channelContainer.waitForMessageCallback();
         Assert.assertEquals(HELLO + JS_MESSAGE, data.getStringValue());
+        // getResponseUrl("") is intentionally used instead of getBaseUrl() because
+        // getResponseUrl("") omits the trailing slash to match the RFC 6454 serialized
+        // origin format (scheme://host:port).
+        Assert.assertEquals(mWebServer.getResponseUrl(""), data.mSenderOrigin);
     }
 
     // Verify that a message port can be used immediately (even if it is in
@@ -765,7 +778,7 @@
         ThreadUtils.runOnUiThreadBlocking(
                 () -> {
                     channel[0].setMessageCallback(
-                            (message, _) -> channelContainer.notifyCalled(message), null);
+                            (message, _, _) -> channelContainer.notifyCalled(message), null);
                     mAwContents.postMessageToMainFrame(
                             new MessagePayload(WEBVIEW_MESSAGE),
                             mWebServer.getBaseUrl(),
@@ -793,7 +806,7 @@
                 () -> {
                     MessagePort[] channel = mAwContents.createMessageChannel();
                     channel[1].setMessageCallback(
-                            (message, _) -> channelContainer.notifyCalled(message), null);
+                            (message, _, _) -> channelContainer.notifyCalled(message), null);
                     channel[0].postMessage(new MessagePayload(HELLO), null);
                 });
         // Wait for the asynchronous response from JS.
@@ -844,7 +857,7 @@
         ThreadUtils.runOnUiThreadBlocking(
                 () -> {
                     channel[0].setMessageCallback(
-                            (message, _) -> channelContainer.notifyCalled(message), null);
+                            (message, _, _) -> channelContainer.notifyCalled(message), null);
                     mAwContents.postMessageToMainFrame(
                             new MessagePayload(WEBVIEW_MESSAGE),
                             mWebServer.getBaseUrl(),
@@ -916,7 +929,7 @@
         ThreadUtils.runOnUiThreadBlocking(
                 () -> {
                     channel[0].setMessageCallback(
-                            (message, _) -> channelContainer.notifyCalled(message), null);
+                            (message, _, _) -> channelContainer.notifyCalled(message), null);
                     mAwContents.postMessageToMainFrame(
                             new MessagePayload(WEBVIEW_MESSAGE),
                             mWebServer.getBaseUrl(),
@@ -969,9 +982,9 @@
                             mWebServer.getBaseUrl(),
                             new MessagePort[] {channel[1]});
                     channel[0].setMessageCallback(
-                            (_, ports) -> {
+                            (_, ports, _) -> {
                                 ports[0].setMessageCallback(
-                                        (message, _) -> {
+                                        (message, _, _) -> {
                                             Assert.assertEquals("3", message.getAsString());
                                             ports[0].postMessage(new MessagePayload("4"), null);
                                         },
@@ -1103,7 +1116,7 @@
                 () -> {
                     MessagePort[] channel = mAwContents.createMessageChannel();
                     channel[0].setMessageCallback(
-                            (message, _) -> channelContainer.notifyCalled(message), null);
+                            (message, _, _) -> channelContainer.notifyCalled(message), null);
                     mAwContents.postMessageToMainFrame(
                             new MessagePayload(WEBVIEW_MESSAGE),
                             mWebServer.getBaseUrl(),
@@ -1135,7 +1148,7 @@
                 () -> {
                     MessagePort[] channel = mAwContents.createMessageChannel();
                     channel[0].setMessageCallback(
-                            (message, _) -> channelContainer.notifyCalled(message), null);
+                            (message, _, _) -> channelContainer.notifyCalled(message), null);
                     mAwContents.postMessageToMainFrame(
                             new MessagePayload(WEBVIEW_MESSAGE),
                             mWebServer.getBaseUrl(),
@@ -1178,7 +1191,7 @@
                 () -> {
                     MessagePort[] channel = mAwContents.createMessageChannel();
                     channel[0].setMessageCallback(
-                            (message, _) -> channelContainer.notifyCalled(message), null);
+                            (message, _, _) -> channelContainer.notifyCalled(message), null);
                     mAwContents.postMessageToMainFrame(
                             new MessagePayload(WEBVIEW_MESSAGE),
                             mWebServer.getBaseUrl(),
@@ -1213,7 +1226,7 @@
                 () -> {
                     MessagePort[] channel = mAwContents.createMessageChannel();
                     channel[0].setMessageCallback(
-                            (message, _) -> channelContainer.notifyCalled(message), null);
+                            (message, _, _) -> channelContainer.notifyCalled(message), null);
                     mAwContents.postMessageToMainFrame(
                             new MessagePayload(WEBVIEW_MESSAGE),
                             mWebServer.getBaseUrl(),
@@ -1250,7 +1263,7 @@
                 () -> {
                     MessagePort[] channel = mAwContents.createMessageChannel();
                     channel[0].setMessageCallback(
-                            (message, _) -> channelContainer.notifyCalled(message), null);
+                            (message, _, _) -> channelContainer.notifyCalled(message), null);
                     mAwContents.postMessageToMainFrame(
                             new MessagePayload(WEBVIEW_MESSAGE),
                             mWebServer.getBaseUrl(),
@@ -1274,9 +1287,9 @@
                 () -> {
                     MessagePort[] channel = mAwContents.createMessageChannel();
                     channel[0].setMessageCallback(
-                            (message, _) -> channelContainer1.notifyCalled(message), null);
+                            (message, _, _) -> channelContainer1.notifyCalled(message), null);
                     channel[1].setMessageCallback(
-                            (message, _) -> channelContainer2.notifyCalled(message),
+                            (message, _, _) -> channelContainer2.notifyCalled(message),
                             new Handler(thread.getLooper()));
                     channel[0].postMessage(new MessagePayload("foo"), null);
                     channel[1].postMessage(new MessagePayload("bar"), null);
@@ -1302,7 +1315,7 @@
                     MessagePort[] channel = mAwContents.createMessageChannel();
                     channelContainer.set(channel);
                     channel[0].setMessageCallback(
-                            (message, _) -> channelContainer.notifyCalled(message),
+                            (message, _, _) -> channelContainer.notifyCalled(message),
                             new Handler(thread.getLooper()));
                     channel[1].postMessage(new MessagePayload("foo"), null);
                 });
@@ -1314,7 +1327,7 @@
                 () -> {
                     MessagePort[] channel = channelContainer.get();
                     channel[0].setMessageCallback(
-                            (message, _) -> channelContainer2.notifyCalled(message), null);
+                            (message, _, _) -> channelContainer2.notifyCalled(message), null);
                     channel[1].postMessage(new MessagePayload("bar"), null);
                 });
         ChannelContainer.Data data2 = channelContainer2.waitForMessageCallback();
@@ -1415,7 +1428,7 @@
 
         final MessagePort[] ports2 = createChannelOnUiThread();
         ports2[0].setMessageCallback(
-                (messagePayload, _) -> {
+                (messagePayload, _, _) -> {
                     ThreadUtils.checkUiThread();
                     container.notifyCalled(messagePayload);
                 },
@@ -1469,7 +1482,7 @@
                         () -> {
                             MessagePort[] ports = mAwContents.createMessageChannel();
                             // Move message port into |receiving| state.
-                            ports[0].setMessageCallback((_, _) -> {}, null);
+                            ports[0].setMessageCallback((_, _, _) -> {}, null);
                             return ports;
                         });
         // Close message channel on another thread, simulate the case where the "finalize" is called
@@ -1497,7 +1510,7 @@
         Assert.assertThrows(
                 "Port transferred, should not able to listen on",
                 IllegalStateException.class,
-                () -> portsToTransfer[0].setMessageCallback((_, _) -> {}, null));
+                () -> portsToTransfer[0].setMessageCallback((_, _, _) -> {}, null));
     }
 
     @Test
@@ -1515,7 +1528,7 @@
         ThreadUtils.runOnUiThreadBlocking(
                 () -> ports[0].postMessage(new MessagePayload("msg1"), null));
         ports[1].setMessageCallback(
-                (messagePayload, _) -> container.notifyCalled(messagePayload), handler);
+                (messagePayload, _, _) -> container.notifyCalled(messagePayload), handler);
         Assert.assertEquals("msg1", container.waitForMessageCallback().getStringValue());
     }
 
@@ -1543,7 +1556,7 @@
                 () -> {
                     MessagePort[] ports = mAwContents.createMessageChannel();
                     ports[0].setMessageCallback(
-                            (message, _) -> container.notifyCalled(message), null);
+                            (message, _, _) -> container.notifyCalled(message), null);
                     mAwContents.postMessageToMainFrame(
                             new MessagePayload("*"), "*", new MessagePort[] {ports[1]});
                     ports = null;
Loading diff…

Original Bug Report

reported by vm...@google.com

Origin impersonation in Custom Tabs postMessage via transferred MessagePorts

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

Overview: Chrome Custom Tabs (CCT) misreports the origin of messages received via MessagePorts by always attributing them to the main frame’s URL. This allows a cross-origin context to impersonate a trusted top-level origin to a host Android application if a MessagePort is transferred to it.

Affected files:

  • chrome/android/java/src/org/chromium/chrome/browser/browserservices/PostMessageHandler.java

Estimated timestamp from git blame: Unknown (Google3 checkout)

Root Cause analysis

In the Chrome Custom Tabs (CCT) implementation on Android, PostMessageHandler.java handles two-way communication between a host app and a web page using a MessagePort. When a message is received from the web page, the handler populates the androidx.browser.customtabs.POST_MESSAGE_ORIGIN bundle extra, which host applications frequently use to make security decisions.

The current implementation in chrome/android/java/src/org/chromium/chrome/browser/browserservices/PostMessageHandler.java determines the origin of an incoming message by querying the current state of the WebContents:

// PostMessageHandler.java:85
Bundle bundle = null;
GURL url = mWebContents.getMainFrame().getLastCommittedURL();
if (url != null) {
    String origin = GURLUtils.getOrigin(url.getSpec());
    bundle = new Bundle();
    bundle.putString(POST_MESSAGE_ORIGIN, origin);
}

This logic assumes that messages delivered over the MessagePort always originate from the primary main frame. However, according to the HTML specification, MessagePort objects are transferable. A trusted top-level page (e.g., https://trusted.example) can receive a port from the host app and transfer it to a cross-origin iframe (e.g., https://evil.example). When the iframe sends a message through this port, PostMessageHandler incorrectly labels it with the origin of the main frame (https://trusted.example).

Vulnerability Mechanism

Our investigation of the underlying layers reveals that Chromium’s C++ layer accurately tracks the sender’s origin. When MessagePort.postMessage is called in the renderer, blink::MessagePort populates the sender_origin field in a blink::TransferableMessage (third_party/blink/renderer/core/messaging/message_port.cc:138).

This origin is preserved through Mojo IPC to the browser process. In content/browser/android/app_web_message_port.cc:159, the Accept method deserializes the message and has access to the correct sender_origin. However, the JNI transition to the Java layer at app_web_message_port.cc:182 fails to pass this origin metadata, leaving the Java PostMessageHandler to rely on the inaccurate WebContents URL.

Potential Impact

An attacker can exploit this origin confusion to impersonate a trusted origin to a host application. If the host application grants sensitive capabilities (e.g., access to local data, execution of native commands, or OAuth token relaying) based on the POST_MESSAGE_ORIGIN extra, a malicious iframe can drive those capabilities under the guise of the trusted main frame.

Potential Steps to Reproduce

  1. Host App Setup: Create an Android app that connects to CustomTabsService and requests a postMessage channel for https://trusted.example. In onPostMessage, log the POST_MESSAGE_ORIGIN extra.
  2. Trusted Page Setup: Serve a page at https://trusted.example that receives the MessagePort and transfers it to a cross-origin iframe (https://evil.example).
  3. Attacker Payload: From the iframe, send a message: port.postMessage("PAYLOAD").
  4. Observation: The host app will receive the message with the origin label https://trusted.example, despite the message originating from https://evil.example.

Suggested Fix

  1. Modify the JNI signature of AppWebMessagePort.onMessage to include a String senderOrigin parameter.
  2. Update content/browser/android/app_web_message_port.cc to extract the sender_origin from the blink::TransferableMessage and pass it to the JNI call.
  3. Update PostMessageHandler.java to use the origin provided by the MessagePort callback rather than querying mWebContents.getMainFrame().getLastCommittedURL().

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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