CVE-2026-113238
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/browserservices/PostMessageHandler.java |
modified |
Files Changed
chrome/android/junit/src/org/chromium/chrome/browser/browserservices/PostMessageHandlerTest.javacontent/public/android/java/src/org/chromium/content_public/browser/MessagePort.javaandroid_webview/glue/java/src/com/android/webview/chromium/WebMessagePortAdapter.javaandroid_webview/javatests/src/org/chromium/android_webview/test/PostMessageTest.javacontent/browser/android/app_web_message_port.ccandroid_webview/support_library/java/src/org/chromium/support_lib_glue/SupportLibWebMessagePortAdapter.javacontent/public/android/java/src/org/chromium/content/browser/AppWebMessagePort.javachrome/android/java/src/org/chromium/chrome/browser/browserservices/PostMessageHandler.java
Patch
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)
Regression Test / PoC
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;
Original Bug Report
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
- Host App Setup: Create an Android app that connects to
CustomTabsServiceand requests apostMessagechannel forhttps://trusted.example. InonPostMessage, log thePOST_MESSAGE_ORIGINextra. - Trusted Page Setup: Serve a page at
https://trusted.examplethat receives theMessagePortand transfers it to a cross-origin iframe (https://evil.example). - Attacker Payload: From the iframe, send a message:
port.postMessage("PAYLOAD"). - Observation: The host app will receive the message with the origin label
https://trusted.example, despite the message originating fromhttps://evil.example.
Suggested Fix
- Modify the JNI signature of
AppWebMessagePort.onMessageto include aString senderOriginparameter. - Update
content/browser/android/app_web_message_port.ccto extract thesender_originfrom theblink::TransferableMessageand pass it to the JNI call. - Update
PostMessageHandler.javato use the origin provided by theMessagePortcallback rather than queryingmWebContents.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.