CVE-2026-3936
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forcomponents/js_injection/renderer/js_communication.cc |
modified | |
ifcomponents/js_injection/renderer/js_communication.cc |
modified | |
JsObjectInfocomponents/js_injection/renderer/js_communication.h |
modified |
Files Changed
components/js_injection/renderer/js_communication.cccomponents/js_injection/renderer/js_communication.h
Patch
From 34de88b274c41f245c28b123e03de9bc03e99163 Mon Sep 17 00:00:00 2001 From: Dave Tapuska <[email protected]> Date: Thu, 12 Feb 2026 09:47:21 -0800 Subject: [PATCH] [webview] Fix an issue around lifecycle of WebLocalFrame Ensure that we monitor destruction of the JsCommunication object. Bug: 481920229 Change-Id: Ibbc6eacf0cb44fc737b0228c5d231d6fddf0caea Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7567944 Commit-Queue: Dave Tapuska <[email protected]> Reviewed-by: Bo Liu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1584064} --- diff --git a/components/js_injection/renderer/js_communication.cc b/components/js_injection/renderer/js_communication.cc index 4bfd8976..984f9f1 100644 --- a/components/js_injection/renderer/js_communication.cc +++ b/components/js_injection/renderer/js_communication.cc @@ -210,22 +210,38 @@ } void JsCommunication::RunScripts(mojom::DocumentInjectionTime injection_time) { - url::Origin frame_origin = - url::Origin(render_frame()->GetWebFrame()->GetSecurityOrigin()); - for (const auto& script : scripts_) { + RunScriptsInternal(weak_ptr_factory_.GetWeakPtr(), injection_time); + // Careful `this` may be destroyed. +} + +// static +void JsCommunication::RunScriptsInternal( + base::WeakPtr<JsCommunication> js_communication, + mojom::DocumentInjectionTime injection_time) { + CHECK(js_communication); + url::Origin frame_origin = url::Origin( + js_communication->render_frame()->GetWebFrame()->GetSecurityOrigin()); + for (const auto& script : js_communication->scripts_) { if (!script->origin_matcher.Matches(frame_origin)) { continue; } if (script->injection_time == injection_time) { if (script->js_world == content::ISOLATED_WORLD_ID_GLOBAL) { - render_frame()->GetWebFrame()->ExecuteScript( + js_communication->render_frame()->GetWebFrame()->ExecuteScript( blink::WebScriptSource(script->script)); } else { - render_frame()->GetWebFrame()->ExecuteScriptInIsolatedWorld( - script->js_world, blink::WebScriptSource(script->script), - blink::BackForwardCacheAware::kAllow); + js_communication->render_frame() + ->GetWebFrame() + ->ExecuteScriptInIsolatedWorld( + script->js_world, blink::WebScriptSource(script->script), + blink::BackForwardCacheAware::kAllow); } } + // Careful, executing a script may cause JsCommunication object to be + // destroyed. + if (!js_communication) { + return; + } } } diff --git a/components/js_injection/renderer/js_communication.h b/components/js_injection/renderer/js_communication.h index deb050f2..797305e 100644 --- a/components/js_injection/renderer/js_communication.h +++ b/components/js_injection/renderer/js_communication.h @@ -61,6 +61,10 @@ class JsObjectInfo; struct JavaScriptExecutable; + static void RunScriptsInternal( + base::WeakPtr<JsCommunication> js_communication, + mojom::DocumentInjectionTime injection_time); + void BindPendingReceiver( mojo::PendingAssociatedReceiver<mojom::JsCommunication> pending_receiver); @@ -80,6 +84,7 @@ mojo::AssociatedRemote<mojom::JsObjectsClient> client_remote_; base::WeakPtrFactory<JsCommunication> weak_ptr_factory_for_bindings_{this}; + base::WeakPtrFactory<JsCommunication> weak_ptr_factory_{this}; }; } // namespace js_injection
Original Bug Report
UAF in JsCommunication, leading to RCE
Steps to reproduce the problem
Tested on Pixel 9 Pro, Chromium version 144.0.7559.109.
- Build
app.zipand Install python -m http.server- Visit
index.html - UAF and Crash
Problem Description
JsCommunication is a content::RenderFrameObserver, which is destructed along with the destruction of the RenderFrameImpl [1].
On iOS and Android platforms, application developers can use interfaces (such as addDocumentStartJavaScript) to inject JavaScript code that runs at DocumentEnd or DocumentStart. JavaScript injected through this interface also runs in iframes. When the JavaScript executes, the scripts in the vector are processed one by one in a for-loop[2],[3].
void JsCommunication::OnDestruct() {
delete this; // [1]
}
void JsCommunication::RunScripts(mojom::DocumentInjectionTime injection_time) {
url::Origin frame_origin =
url::Origin(render_frame()->GetWebFrame()->GetSecurityOrigin());
for (const auto& script : scripts_) { // [2]
if (!script->origin_matcher.Matches(frame_origin)) {
continue;
}
if (script->injection_time == injection_time) {
if (script->js_world == content::ISOLATED_WORLD_ID_GLOBAL) {
render_frame()->GetWebFrame()->ExecuteScript(
blink::WebScriptSource(script->script));
} else {
render_frame()->GetWebFrame()->ExecuteScriptInIsolatedWorld( // [3]
script->js_world, blink::WebScriptSource(script->script),
blink::BackForwardCacheAware::kAllow);
}
}
}
}
The injected JavaScript can easily be redirected to code controlled by an attacker, for example, through getter, setter, or redefining functions in the prototype. An attacker can then remove the iframe, causing the RenderFrameImpl to be destroyed, which in turn destroys the JsCommunication object. When the next loop attempts to execute JavaScript, a UAF occurs, which is not protected by miraclePtr
Although triggering this UAF requires certain preconditions, in practice, many browsers (such as the Pawxy browser) utilize this interface, and it is also widely used by third-party libraries (e.g., react-native-webview).
app.zipis a simple WebView-based browser that injects two JavaScript codes via theaddDocumentStartJavaScriptinterface.
private void injectStartUpScripts() {
if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
Set<String> allowedOriginRules = Collections.singleton("*");
try {
WebViewCompat.addDocumentStartJavaScript(
webView,
"console.log(`${new Number(1.1)}`);",
allowedOriginRules
);
// WebViewCompat.addDocumentStartJavaScript(
// webView,
// "console.log(global_value)",
// allowedOriginRules
// );
WebViewCompat.addDocumentStartJavaScript(
webView,
"console.log('Second Script Injected');",
allowedOriginRules
);
} catch (Exception e) {
Log.e(TAG, "addDocumentStartJavaScript failed");
}
} else {
Toast.makeText(this, "addDocumentStartJavaScript not supported", Toast.LENGTH_SHORT).show();
}
}
- Using this browser to visit the attacker’s webpage.
- The attacker’s webpage redefines
Number.prototype.toStringinside an iframe.
node.contentWindow.Number.prototype.toString = function () {
node.remove();
};
- The first injected JavaScript executes and triggers the attacker’s code, which removes the iframe, ultimately leading to the destruction of
JsCommunication. - In the next iteration of the loop, a UAF occurs.
Summary
UAF in JsCommunication, leading to RCE
Custom Questions
Reporter credit:
Am4deu$
Additional Data
Category: Security
Chrome Channel: Stable
Regression: N/A \
- https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E
- https://developer.android.com/reference/androidx/webkit/WebViewCompat#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set<java.lang.String