CVE-2026-102327
Overview
Background
- `WebMessageListener` / `sourceOrigin`
- An Android WebView API where the app registers a JavaScript-to-app message channel and receives, with each message, a
sourceOriginthe app uses to decide whether to trust the message. - `JsToBrowserMessaging`
- The browser-process endpoint (in
components/js_injection/browser) that receivesPostMessagemojo calls from the renderer and forwards them to the app’sWebMessageHost, carrying the frame’s origin and main-frame status. - `DCHECK_EQ` vs `mojo::ReportBadMessage`
DCHECK_EQis compiled out of release builds (only active whenDCHECK_IS_ON()), whereasmojo::ReportBadMessagealways runs and kills a misbehaving renderer; the two guard renderer-supplied data very differently in production.- Cached origin/frame members
top_level_origin_string_,origin_string_, andis_main_frame_, snapshotted on the firstPostMessageand meant to be re-checked against later calls.
Root Cause Analysis
In the PostMessage handler of JsToBrowserMessaging (name not shown in the @@ -254 hunk but established by the class and the mojo method), the origin/frame values are captured on the first call (when host_ is created) and are supposed to remain invariant for the life of the connection.
Pre-patch, both the cached members (top_level_origin_string_, origin_string_, is_main_frame_) and the consistency checks were wrapped in #if DCHECK_IS_ON(), so in production builds the members did not exist and the DCHECK_EQ comparisons were compiled out entirely. A compromised or malicious renderer could therefore send a first PostMessage establishing a legitimate origin and then send subsequent PostMessage calls carrying a different source_origin/top_level_origin, which the browser forwarded to the app’s WebMessageListener as the sourceOrigin without any release-build check — spoofing the origin an app relies on for authorization.
The fix makes the three members unconditional and replaces each DCHECK_EQ with an always-enforced if (mismatch) { mojo::ReportBadMessage(...); return; }, so a later call whose origin or frame status differs from the cached values is rejected (and the renderer killed) in production; it also reorders the if (!host_) return; check to run before the members are populated. As the added comment notes, the checks are effective from the second call onward, pinning the origin to the value captured when the host was first created.
DCHECK_EQ (and storing the cached values only under DCHECK_IS_ON()), so the anti-spoofing check simply did not exist in shipping builds; the fix promotes the checks to always-on mojo::ReportBadMessage and always populates the cached values.Attack Path
- Obtain renderer control of the message pipe
A compromised or malicious renderer holds the
JsToBrowserMessagingmojo endpoint and controls thesource_origin,top_level_origin, and frame arguments of eachPostMessage. - Establish a trusted host
The renderer sends a first
PostMessagewith a legitimate origin, causingCreateHost(...)to succeed and (post-patch) the cached origin/frame members to be recorded. - Send a spoofed follow-up message
On a subsequent
PostMessage, the renderer supplies a differentsource_origin/top_level_origin(or flipsis_main_frame); pre-patch, the debug-onlyDCHECK_EQwas compiled out, so the mismatch was not detected in release builds. - App receives a spoofed `sourceOrigin`
The forged origin is delivered to the app’s
WebMessageListeneras the message’ssourceOrigin, so app logic that authorizes messages by origin (e.g. an allowlist) can be tricked into trusting a message from an origin that did not send it.
Impact Assessment
sourceOrigin reported to a WebView app’s WebMessageListener, defeating origin-based authorization decisions the app makes in the browser process. The vendor advisory rates this low severity and does not report in-the-wild exploitation; NVD scores it CVSS 7.5. Any concrete consequence within a specific embedding app (data access, privileged action) is (inferred) and depends on how that app trusts sourceOrigin. The precondition is renderer control over the PostMessage arguments after a host has been established.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/js_injection/browser/js_to_browser_messaging.cc |
modified |
Files Changed
components/js_injection/browser/js_to_browser_messaging.cccomponents/js_injection/browser/js_to_browser_messaging.h
Audit Directions
- DCHECK-guarded validation of IPC dataSearch for
DCHECK/DCHECK_EQused to “validate” renderer- or other-process-supplied values across a trust boundary; these vanish in release and should bemojo::ReportBadMessage/ReceivedBadMessage. - State cached only under `DCHECK_IS_ON()`Look for member variables compiled only in debug builds that a security check depends on, since the check silently becomes a no-op in production.
- Immutability assumptions on per-connection identityAudit mojo endpoints that snapshot an origin/frame/identity once and assume it cannot change, verifying the invariant is enforced on every subsequent call rather than only the first.
Patch
From a43e3a87de7e4b908bb10b24237239431cd1f2dd Mon Sep 17 00:00:00 2001
From: Abhijith Nair <abhijithnair@chromium.org>
Date: Fri, 19 Jun 2026 03:22:27 -0700
Subject: [PATCH] Make it harder to spoof WebMessageListener sourceOrigin in WV
We now compare cached origin against current in production builds. The
member variables storing origin and frame information are now always
populated. The consistency checks for these values are upgraded from
DCHECK_EQ to mojo::ReportBadMessage, ensuring they are always enforced.
Bug: 496212975
Change-Id: Ia87e7803d1dd414373700f4faa41ee2031a2a135
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7943265
Reviewed-by: Peter Pakkenberg <pbirk@chromium.org>
Commit-Queue: Abhijith Nair <abhijithnair@chromium.org>
Reviewed-by: Richard Coles <torne@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1649582}
---
diff --git a/components/js_injection/browser/js_to_browser_messaging.cc b/components/js_injection/browser/js_to_browser_messaging.cc
index b785940..b7b06ea 100644
--- a/components/js_injection/browser/js_to_browser_messaging.cc
+++ b/components/js_injection/browser/js_to_browser_messaging.cc
@@ -20,6 +20,7 @@
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/weak_document_ptr.h"
#include "content/public/browser/web_contents.h"
+#include "mojo/public/cpp/bindings/message.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/common/messaging/message_port_descriptor.h"
#include "third_party/blink/public/common/messaging/string_message_codec.h"
@@ -254,21 +255,32 @@
host_ =
connection_factory_->CreateHost(top_level_origin_string, origin_string,
is_main_frame, reply_proxy_.get());
-#if DCHECK_IS_ON()
+ if (!host_) {
+ return;
+ }
+
top_level_origin_string_ = top_level_origin_string;
origin_string_ = origin_string;
is_main_frame_ = is_main_frame;
-#endif
- if (!host_)
- return;
}
+
+ // The following checks will only be effective on the second and following
+ // calls to `PostMessage`.
// The origin and whether this is the main frame should not change once
// PostMessage() has been received.
-#if DCHECK_IS_ON()
- DCHECK_EQ(GetOriginString(top_level_origin), top_level_origin_string_);
- DCHECK_EQ(GetOriginString(source_origin), origin_string_);
- DCHECK_EQ(is_main_frame_, !render_frame_host_->GetParentOrOuterDocument());
-#endif
+ if (top_level_origin_string_ != GetOriginString(top_level_origin)) {
+ mojo::ReportBadMessage("top_level_origin changed after host reset.");
+ return;
+ }
+ if (origin_string_ != GetOriginString(source_origin)) {
+ mojo::ReportBadMessage("source_origin changed after host reset.");
+ return;
+ }
+ if (is_main_frame_ != !render_frame_host_->GetParentOrOuterDocument()) {
+ mojo::ReportBadMessage("is_main_frame changed after host reset.");
+ return;
+ }
+
std::unique_ptr<WebMessage> web_message = std::make_unique<WebMessage>();
web_message->message = std::move(message);
web_message->ports = std::move(ports);
diff --git a/components/js_injection/browser/js_to_browser_messaging.h b/components/js_injection/browser/js_to_browser_messaging.h
index 14217a4..0b3a716b 100644
--- a/components/js_injection/browser/js_to_browser_messaging.h
+++ b/components/js_injection/browser/js_to_browser_messaging.h
@@ -72,11 +72,9 @@
std::unique_ptr<WebMessageHost> host_;
mojo::SharedAssociatedRemote<mojom::BrowserToJsMessagingFactory>
browser_to_js_factory_;
-#if DCHECK_IS_ON()
std::string top_level_origin_string_;
std::string origin_string_;
bool is_main_frame_;
-#endif
};
} // namespace js_injection