Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebView
DescriptionUse after free in WebView
ComponentWebView
Bug ClassUAF
Tracker481920229
Fix commit34de88b274c4 (chromium/src) +28/-7
CISA KEVNot listed
CreditedAm4deu$
Disclosed2026-03-10

Changed Functions

FunctionChangeNotes
for
components/js_injection/renderer/js_communication.cc
modified
if
components/js_injection/renderer/js_communication.cc
modified
JsObjectInfo
components/js_injection/renderer/js_communication.h
modified

Files Changed

  • components/js_injection/renderer/js_communication.cc
  • components/js_injection/renderer/js_communication.h
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
Loading diff…

Original Bug Report

reported by [email protected]

UAF in JsCommunication, leading to RCE

Steps to reproduce the problem

Tested on Pixel 9 Pro, Chromium version 144.0.7559.109.

  1. Build app.zip and Install
  2. python -m http.server
  3. Visit index.html
  4. 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).

  1. app.zip is a simple WebView-based browser that injects two JavaScript codes via the addDocumentStartJavaScript interface.
    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();
        }
    }
  1. Using this browser to visit the attacker’s webpage.
  2. The attacker’s webpage redefines Number.prototype.toString inside an iframe.
node.contentWindow.Number.prototype.toString = function () {
  node.remove();
};
  1. The first injected JavaScript executes and triggers the attacker’s code, which removes the iframe, ultimately leading to the destruction of JsCommunication.
  2. 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 \

View on issue tracker