Overview

High
Severity
8.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Extensions
DescriptionMissing authorization in Extensions
ComponentExtensions
Sub-componentExtensions
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: Diff's exploit browsertests simulate a misbehaving renderer and the fix uses a bad-message kill, implying a compromised renderer; 'only precondition'/default overstates. What RendererAutomationRegistry grants is not shown in the diff.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker540265100
Fix commitb70c13072d35 (chromium/src) +77/-0
CWECWE-862
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N
CISA KEVNot listed
CreditedOGINOME Tomohito
Reported2026-07-29 · bounty $5,000 (per vendor advisory)
Fix landed2026-08-18 (public git)
Disclosed2026-09-22
Patch gap35 days — fix public in source before the advisory

Background

`AutomationEventRouter::BindForRenderer`
The browser-process entry point that binds a renderer’s mojom::RendererAutomationRegistry interface so the renderer can drive the accessibility automation API.
`ProcessMap`
A per-BrowserContext registry that maps a renderer process to the enabled extension it hosts, queried here via GetEnabledExtensionByProcessID.
`AutomationInfo`
The manifest handler data that indicates an extension declared the automation permission, retrieved with AutomationInfo::Get.
`bad_message::ReceivedBadMessage`
The extensions IPC-safety helper that terminates a renderer process sending an unauthorized or malformed message.

Root Cause Analysis

The Mojo interface exposed by AutomationEventRouter::BindForRenderer was bound for any renderer that requested it, without confirming that the calling process actually hosted an enabled extension holding the automation permission. Because the browser trusted the renderer-initiated bind request implicitly, a non-extension renderer (a web page) or an extension process lacking the permission could obtain the privileged RendererAutomationRegistry interface, violating the invariant that automation registration is reserved to authorized automation extensions.

The fix resolves the calling process with content::RenderProcessHost::FromID, looks up the hosted extension through ProcessMap::GetEnabledExtensionByProcessID, and confirms AutomationInfo::Get returns non-null before proceeding. If any of these checks fail, the code calls bad_message::ReceivedBadMessage with the new AER_INVALID_PROCESS_FOR_AUTOMATION_BINDING reason and returns, so the interface is never bound for an unauthorized process. This works because authorization is now enforced in the browser process, which cannot be bypassed by a compromised or misbehaving renderer.

Key insight
The single mistake was performing a privileged Mojo interface bind on behalf of a renderer without validating that the renderer was an authorized automation extension; the fix adds that missing authorization gate before binding and kills any process that fails it.

Attack Path

  1. Obtain a renderer An attacker controls or navigates a renderer process, either an ordinary web page or an extension process that never declared the automation permission.
  2. Request the automation interface From that renderer, the attacker issues the Mojo request that reaches AutomationEventRouter::BindForRenderer, which previously performed no caller authorization check.
  3. Receive the privileged binding The browser binds mojom::RendererAutomationRegistry for the unauthorized process, exposing accessibility automation registration it should not have.
  4. Abuse automation access The attacker uses the improperly granted interface to interact with the automation event routing that is meant to be restricted to permitted extensions, escalating beyond its intended privilege boundary.

Impact Assessment

An attacker running in an unauthorized renderer (a web page or an under-privileged extension process) could bind the automation registry interface and gain access to accessibility automation functionality normally gated behind the automation permission, constituting a missing-authorization privilege boundary bypass. The only precondition is the ability to issue the bind request from a renderer; no automation permission or extension trust is required in the unpatched code. After the fix, such an attempt instead terminates the offending renderer via a bad-message kill.

Changed Functions

FunctionChangeNotes
if
extensions/browser/api/automation_internal/automation_event_router.cc
modified

Files Changed

  • chrome/browser/extensions/extension_security_exploit_browsertest.cc
  • tools/metrics/histograms/metadata/stability/enums.xml
  • extensions/browser/bad_message.h
  • extensions/browser/api/automation_internal/automation_event_router.cc

Audit Directions

  • Renderer-initiated Mojo binds
    Review other browser-side BindForRenderer-style entry points that bind privileged interfaces and confirm each verifies the calling process’s extension and permission before binding.
  • Permission-gated APIs without process checks
    Look for APIs guarded only by a documented permission but not enforced against ProcessMap::GetEnabledExtensionByProcessID, since the renderer identity, not just the API surface, must be validated.
  • Missing bad-message kills
    Audit browser IPC handlers that silently return on unexpected callers instead of calling bad_message::ReceivedBadMessage, as silent failure can hide missing authorization that a security-sensitive path should reject loudly.
From b70c13072d35070057ca8731d17e2dbe8ba5211a Mon Sep 17 00:00:00 2001
From: David Tseng <dtseng@google.com>
Date: Tue, 18 Aug 2026 11:07:53 -0700
Subject: [PATCH] [Extensions] Validate process authorization in AutomationEventRouter::BindForRenderer

This CL adds a check against ProcessMap and AutomationInfo in
AutomationEventRouter::BindForRenderer. If the calling process is not
hosting an enabled extension with automation permission, the request
is rejected and the renderer process is terminated with a bad message.

Fixed: 540265100
TAG=agy
CONV=0fba6596-b871-40d8-8ac4-a6d2854a4d9c

Change-Id: I107134d7419d5b797a3068932972feb097bb3f9c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8259787
Reviewed-by: Andrea Orru <andreaorru@chromium.org>
Reviewed-by: Alexei Svitkine <asvitkine@chromium.org>
Reviewed-by: Devlin Cronin <rdevlin.cronin@chromium.org>
Commit-Queue: David Tseng <dtseng@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1681537}
---
diff --git a/extensions/browser/api/automation_internal/automation_event_router.cc b/extensions/browser/api/automation_internal/automation_event_router.cc
index 160691e..c4bb2ae3 100644
--- a/extensions/browser/api/automation_internal/automation_event_router.cc
+++ b/extensions/browser/api/automation_internal/automation_event_router.cc
@@ -16,12 +16,15 @@
 #include "build/chromeos_buildflags.h"
 #include "extensions/browser/api/automation_internal/automation_internal_api_delegate.h"
 #include "extensions/browser/api/extensions_api_client.h"
+#include "extensions/browser/bad_message.h"
 #include "extensions/browser/event_router.h"
 #include "extensions/browser/process_manager.h"
+#include "extensions/browser/process_map.h"
 #include "extensions/browser/service_worker/worker_id.h"
 #include "extensions/common/api/automation_internal.h"
 #include "extensions/common/extension.h"
 #include "extensions/common/extension_id.h"
+#include "extensions/common/manifest_handlers/automation.h"
 #include "mojo/public/cpp/bindings/associated_remote.h"
 #include "mojo/public/cpp/bindings/pending_associated_remote.h"
 #include "ui/accessibility/ax_action_data.h"
@@ -349,6 +352,27 @@
     RenderProcessHostId render_process_id,
     mojo::PendingAssociatedReceiver<
         extensions::mojom::RendererAutomationRegistry> receiver) {
+  content::RenderProcessHost* host =
+      content::RenderProcessHost::FromID(render_process_id);
+  if (!host) {
+    return;
+  }
+
+  ProcessMap* process_map = ProcessMap::Get(host->GetBrowserContext());
+  const Extension* extension =
+      process_map
+          ? process_map->GetEnabledExtensionByProcessID(render_process_id)
+          : nullptr;
+
+  const AutomationInfo* automation_info =
+      extension ? AutomationInfo::Get(extension) : nullptr;
+
+  if (!automation_info) {
+    bad_message::ReceivedBadMessage(
+        host, bad_message::AER_INVALID_PROCESS_FOR_AUTOMATION_BINDING);
+    return;
+  }
+
   AutomationEventRouter* router = AutomationEventRouter::GetInstance();
   CHECK(router);
 
diff --git a/extensions/browser/bad_message.h b/extensions/browser/bad_message.h
index df3f2181..ce97edc7 100644
--- a/extensions/browser/bad_message.h
+++ b/extensions/browser/bad_message.h
@@ -80,6 +80,7 @@
   WRA_INVALID_LISTENER_REGISTRATION = 40,
   WER_UNEXPECTED_EVENT_HANDLING_DONE = 41,
   WER_INVALID_EXTENSION_ID = 42,
+  AER_INVALID_PROCESS_FOR_AUTOMATION_BINDING = 43,
   // Please add new elements here. The naming convention is abbreviated class
   // name (e.g. ExtensionHost becomes EH) plus a unique description of the
   // reason. After making changes, you MUST update histograms.xml by running:
diff --git a/tools/metrics/histograms/metadata/stability/enums.xml b/tools/metrics/histograms/metadata/stability/enums.xml
index cfeff6ec..b72aadf1 100644
--- a/tools/metrics/histograms/metadata/stability/enums.xml
+++ b/tools/metrics/histograms/metadata/stability/enums.xml
@@ -603,6 +603,7 @@
   <int value="40" label="WRA_INVALID_LISTENER_REGISTRATION"/>
   <int value="41" label="WER_UNEXPECTED_EVENT_HANDLING_DONE"/>
   <int value="42" label="WER_INVALID_EXTENSION_ID"/>
+  <int value="43" label="AER_INVALID_PROCESS_FOR_AUTOMATION_BINDING"/>
 </enum>
 
 <enum name="BadMessageReasonGuestView">
diff --git a/chrome/browser/extensions/extension_security_exploit_browsertest.cc b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
index 0fe3eb6..ffbac84 100644
--- a/chrome/browser/extensions/extension_security_exploit_browsertest.cc
+++ b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
@@ -30,6 +30,7 @@
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/test_navigation_observer.h"
+#include "extensions/browser/api/automation_internal/automation_event_router.h"
 #include "extensions/browser/api/messaging/channel_endpoint.h"
 #include "extensions/browser/api/messaging/message_service.h"
 #include "extensions/browser/api/storage/storage_api.h"
@@ -47,6 +48,7 @@
 #include "extensions/common/api/messaging/port_id.h"
 #include "extensions/common/constants.h"
 #include "extensions/common/extension_features.h"
+#include "extensions/common/mojom/automation_registry.mojom.h"
 #include "extensions/common/mojom/event_router.mojom.h"
 #include "extensions/common/mojom/frame.mojom-test-utils.h"
 #include "extensions/common/mojom/message_port.mojom.h"
@@ -2216,4 +2218,53 @@
   run_loop.Run();
 }
 
+// Tests that attempting to bind the automation registry interface from an
+// unauthorized process (such as a web page) results in the process being
+// terminated.
+IN_PROC_BROWSER_TEST_F(ExtensionSecurityExploitBrowserTest,
+                       Automation_BindAutomationFromWebPage) {
+  GURL test_page_url =
+      embedded_test_server()->GetURL("foo.com", "/title1.html");
+  auto* web_contents = GetActiveWebContents();
+  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));
+
+  content::RenderProcessHost* main_frame_process =
+      web_contents->GetPrimaryMainFrame()->GetProcess();
+
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
+
+  mojo::AssociatedRemote<mojom::RendererAutomationRegistry> registry;
+  AutomationEventRouter::BindForRenderer(
+      main_frame_process->GetID(),
+      registry.BindNewEndpointAndPassDedicatedReceiver());
+
+  EXPECT_EQ(bad_message::AER_INVALID_PROCESS_FOR_AUTOMATION_BINDING,
+            kill_waiter.Wait());
+}
+
+// Tests that attempting to bind the automation registry interface from an
+// extension process that does not have the "automation" manifest permission
+// results in the process being terminated.
+IN_PROC_BROWSER_TEST_F(
+    ExtensionSecurityExploitBrowserTest,
+    Automation_BindAutomationFromExtensionWithoutPermission) {
+  InstallTestExtensions();
+  GURL extension_page_url = active_extension().GetResourceURL("page.html");
+  auto* web_contents = GetActiveWebContents();
+  ASSERT_TRUE(NavigateToURL(web_contents, extension_page_url));
+
+  content::RenderProcessHost* extension_process =
+      web_contents->GetPrimaryMainFrame()->GetProcess();
+
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(extension_process);
+
+  mojo::AssociatedRemote<mojom::RendererAutomationRegistry> registry;
+  AutomationEventRouter::BindForRenderer(
+      extension_process->GetID(),
+      registry.BindNewEndpointAndPassDedicatedReceiver());
+
+  EXPECT_EQ(bad_message::AER_INVALID_PROCESS_FOR_AUTOMATION_BINDING,
+            kill_waiter.Wait());
+}
+
 }  // namespace extensions
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/extensions/extension_security_exploit_browsertest.cc b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
index 0fe3eb6..ffbac84 100644
--- a/chrome/browser/extensions/extension_security_exploit_browsertest.cc
+++ b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
@@ -30,6 +30,7 @@
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/test_navigation_observer.h"
+#include "extensions/browser/api/automation_internal/automation_event_router.h"
 #include "extensions/browser/api/messaging/channel_endpoint.h"
 #include "extensions/browser/api/messaging/message_service.h"
 #include "extensions/browser/api/storage/storage_api.h"
@@ -47,6 +48,7 @@
 #include "extensions/common/api/messaging/port_id.h"
 #include "extensions/common/constants.h"
 #include "extensions/common/extension_features.h"
+#include "extensions/common/mojom/automation_registry.mojom.h"
 #include "extensions/common/mojom/event_router.mojom.h"
 #include "extensions/common/mojom/frame.mojom-test-utils.h"
 #include "extensions/common/mojom/message_port.mojom.h"
@@ -2216,4 +2218,53 @@
   run_loop.Run();
 }
 
+// Tests that attempting to bind the automation registry interface from an
+// unauthorized process (such as a web page) results in the process being
+// terminated.
+IN_PROC_BROWSER_TEST_F(ExtensionSecurityExploitBrowserTest,
+                       Automation_BindAutomationFromWebPage) {
+  GURL test_page_url =
+      embedded_test_server()->GetURL("foo.com", "/title1.html");
+  auto* web_contents = GetActiveWebContents();
+  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));
+
+  content::RenderProcessHost* main_frame_process =
+      web_contents->GetPrimaryMainFrame()->GetProcess();
+
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
+
+  mojo::AssociatedRemote<mojom::RendererAutomationRegistry> registry;
+  AutomationEventRouter::BindForRenderer(
+      main_frame_process->GetID(),
+      registry.BindNewEndpointAndPassDedicatedReceiver());
+
+  EXPECT_EQ(bad_message::AER_INVALID_PROCESS_FOR_AUTOMATION_BINDING,
+            kill_waiter.Wait());
+}
+
+// Tests that attempting to bind the automation registry interface from an
+// extension process that does not have the "automation" manifest permission
+// results in the process being terminated.
+IN_PROC_BROWSER_TEST_F(
+    ExtensionSecurityExploitBrowserTest,
+    Automation_BindAutomationFromExtensionWithoutPermission) {
+  InstallTestExtensions();
+  GURL extension_page_url = active_extension().GetResourceURL("page.html");
+  auto* web_contents = GetActiveWebContents();
+  ASSERT_TRUE(NavigateToURL(web_contents, extension_page_url));
+
+  content::RenderProcessHost* extension_process =
+      web_contents->GetPrimaryMainFrame()->GetProcess();
+
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(extension_process);
+
+  mojo::AssociatedRemote<mojom::RendererAutomationRegistry> registry;
+  AutomationEventRouter::BindForRenderer(
+      extension_process->GetID(),
+      registry.BindNewEndpointAndPassDedicatedReceiver());
+
+  EXPECT_EQ(bad_message::AER_INVALID_PROCESS_FOR_AUTOMATION_BINDING,
+            kill_waiter.Wait());
+}
+
 }  // namespace extensions
Loading diff…

Original Bug Report

reported by og...@gmail.com

Site Isolation bypass: the ungated extensions RendererAutomationRegistry handler lets a compromised web renderer read cross-origin/cross-profile accessibility trees (page text, frame URLs).


Report description

Site Isolation bypass: the ungated extensions RendererAutomationRegistry handler lets a compromised web renderer read cross-origin/cross-profile accessibility trees (page text, frame URLs).


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://chromium.googlesource.com/chromium/src/+/HEAD/extensions/browser/api/automation_internal/automation_event_router.cc


The problem

Please describe the technical details of the vulnerability

Extensions automation registry: a compromised web renderer joins the accessibility broadcast and reads cross-origin / cross-profile page content (Site Isolation bypass)

TL;DR

The browser-side handler for the extensions associated interface extensions.mojom.RendererAutomationRegistry (BindAutomation) performs no caller validation, yet it is registered for every RenderFrameHost — including ordinary web frames. A compromised, site-locked web renderer can therefore register its own ax.mojom.Automation receiver into the process-global accessibility broadcast set.

Impact: while any legitimate chrome.automation consumer (e.g. ChromeVox on ChromeOS) is active, the browser broadcasts every tab’s full serialized accessibility tree — visible text, non-password form-field values, ARIA, cross-site frame URLs — to that rogue receiver. That is a cross-origin content read from a renderer Site Isolation is supposed to contain. Because the handler is a process-global singleton with no BrowserContext key, in source the reach also spans profiles including Incognito (the reproducer exercises the cross-origin case within one profile).

The sibling interface extensions.mojom.EventRouter, registered by the identical mechanism two lines below, validates the caller process (ProcessMap + CanAccessDataForOrigin + bad-message); automation does not.

Severity

Chrome VRP High per the rubric (https://chromium.googlesource.com/chromium/src/+/main/docs/security/severity-guidelines.md) — a Site Isolation bypass / cross-site data disclosure (severity-guidelines.md, “Web Platform & UX”: “Site Isolation bypasses: … Cross-site data disclosure”). The affected data (cross-origin page text, form-field values, URLs) is exactly the asset docs/security/compromised-renderers.md commits to protect from a compromised renderer.

Affected component

extensions/browser/api/automation_internal/automation_event_router.cc (AutomationEventRouter::BindAutomation / BindForRenderer / DispatchAccessibilityEvents); registration at chrome/browser/extensions/chrome_content_browser_client_extensions_part_bindings.cc:70-72. Verified on origin/main (Chrome 152, 2026-07). Default Stable configuration; no experimental flag.

Attack scenario

  • Attacker: code executing in a compromised renderer process — the standard Site Isolation threat model (docs/security/compromised-renderers.md: a compromised renderer “can forge malicious IPC messages, impersonate a Chrome Extension content script”). The process is site-locked to the attacker’s own origin.
  • What the attacker gains: read access to the full accessibility tree (visible text, non-password form-field values, ARIA, cross-site frame URLs) of other origins and other tabs — data the attacker’s process is isolated from. The handler is a process-global singleton with no profile key, so in source the reach also spans other profiles including Incognito (the attached reproducer exercises the cross-origin case within one profile).
  • How: the renderer requests the associated interface extensions.mojom.RendererAutomationRegistry from its own frame and calls BindAutomation, passing an Automation receiver it controls. The browser adds that receiver to the process-global automation_remote_set_ with no check. From then on the browser pushes every serialized tree to it.
  • Prior access required: a single prior renderer compromise (the premise of the Site Isolation threat model). No extension permission, no user interaction on the attacker side.
  • Victim assumption: a legitimate chrome.automation consumer is active. On ChromeOS this is routine (ChromeVox, Select-to-Speak, Switch Access are chrome.automation component extensions). Per severity-guidelines.md, that a bug “applies only to … those users running with accessibility features enabled” is explicitly not a mitigating factor.

The defect — an unenforced trust assumption its own sibling enforces

Extension associated interfaces are intentionally exposed to all renderer processes, because extension content scripts run inside web renderers. The security model therefore relies on browser-side per-caller validation. docs/security/ax-tree-security-guidelines.md states the invariant directly:

> “The tree … holds sensitive content that should not be passed back down to renderers to avoid data leaks between them.” > “Some fields … represent sensitive data from the origin … and should not be shared into unrelated renderer processes.”

EventRouter enforces this: EventRouter::BindForRenderer stores the calling render_process_id as receiver context, and every message validates it via ProcessMap::Get(browser_context_)->Contains(extension_id, process) + ChildProcessSecurityPolicy::CanAccessDataForOrigin, killing an unauthorized caller with ReportBadMessage (extensions/browser/event_router.cc:408-465).

AutomationEventRouter::BindForRenderer stores the identical render_process_id context (automation_event_router.cc:355) — but BindAutomation never reads it:

void AutomationEventRouter::BindAutomation(
    mojo::PendingAssociatedRemote<ax::mojom::Automation> automation) {
  automation_remote_set_.Add(std::move(automation));   // no caller check
}

The permission/tree-scope bookkeeping that should gate delivery (listeners_, populated only by the permission-gated Register/EnableTree extension functions) is a separate structure the dispatch loop never consults:

void AutomationEventRouter::DispatchAccessibilityEvents(...) {
  ...
  for (const auto& remote : automation_remote_set_)          // every remote
    remote->DispatchAccessibilityEvents(tree_id, updates, ...); // every tree
}

So the browser passes the full cross-origin/cross-profile tree “back down to renderers” — the precise leak the security guidelines forbid — because the caller is never validated.

Reachability — a compromised web frame’s request routes to the unvalidated handler

The attacker’s sole capability is a compromised renderer forging an associated-interface request by name (docs/security/compromised-renderers.md). Below is the entire browser-side path from that request to the unvalidated BindAutomation, quoted from origin/main (Chrome 152).

1. The extensions associated interfaces are registered for every RenderFrameHost, unconditionally — chrome/browser/chrome_content_browser_client_receiver_bindings.cc:400-407:

void ChromeContentBrowserClient::
    RegisterAssociatedInterfaceBindersForRenderFrameHost(
        content::RenderFrameHost& render_frame_host,
        blink::AssociatedInterfaceRegistry& associated_registry) {
  for (auto& ep : extra_parts_) {
    ep->ExposeInterfacesToRendererForRenderFrameHost(render_frame_host,
                                                     associated_registry);
  }

No frame-type / scheme / origin filter guards the loop. (Chrome gates registration by extension scheme where it means to — the service-worker broker path at :372-383 early-returns if (!site.SchemeIs(extensions::kExtensionScheme)) — and deliberately does not here, because extension content scripts execute inside ordinary web renderers.)

2. That set includes RendererAutomationRegistry, bound with the frame’s own process id, no gate — chrome/browser/extensions/chrome_content_browser_client_extensions_part_bindings.cc:66-75:

  content::ChildProcessId render_process_id = frame_host.GetProcess()->GetID();
  ...
  associated_registry.AddInterface<mojom::RendererAutomationRegistry>(
      base::BindRepeating(&AutomationEventRouter::BindForRenderer,
                          render_process_id));
  ...
  associated_registry.AddInterface<mojom::EventRouter>(
      base::BindRepeating(&EventRouter::BindForRenderer, render_process_id));

render_process_id is the browser’s own view of the calling frame’s process — auto-injected into the handler, unforgeable by the attacker. The sibling EventRouter is registered just below by the identical idiom (the elided ... is the RendererHost registration and an #if BUILDFLAG(ENABLE_EXTENSIONS) guarding automation — a build flag that is on for every desktop/ChromeOS Chrome, i.e. wherever the automation system exists at all, not a runtime gate): the exposure is intentional and shared, the caller id is trustworthy; the only question is whether the handler reads it.

3. A live frame’s request reaches the handler by name lookup, with no security gate — content/browser/renderer_host/render_frame_host_impl.cc:10299-10318 (GetAssociatedInterface, the browser end of the frame’s AssociatedInterfaceProvider):

void RenderFrameHostImpl::GetAssociatedInterface(
    const std::string& name,
    mojo::PendingAssociatedReceiver<blink::mojom::AssociatedInterface>
        receiver) {
  CHECK(associated_registry_);
  if (mojo_binder_policy_applier_ &&      // prerender capability control only
      !mojo_binder_policy_applier_->ApplyPolicyToAssociatedBinder(name)) {
    return;
  }
  mojo::ScopedInterfaceEndpointHandle handle = receiver.PassHandle();
  if (associated_registry_->TryBindInterface(name, &handle)) {
    return;
  }
}

The only pre-bind check, mojo_binder_policy_applier_, is non-null only while prerendering (null for a live, committed frame); no process-type, scheme, or origin check exists. associated_registry_ is the frame’s single registry, created in SetUpMojoConnection and handed to the step-1 embedder hook — render_frame_host_impl_interface_binders.cc:176 then :298-302 (GetContentClient()->browser()->RegisterAssociatedInterfaceBindersForRenderFrameHost(*this, *associated_registry_)) — so it is exactly the registry steps 1-2 populate. TryBindInterface(name, …) therefore resolves the step-2 callback: AutomationEventRouter::BindForRenderer(render_process_id, receiver).

4. The handler stores the caller id, then never reads it — extensions/browser/api/automation_internal/automation_event_router.cc:347-356 then :342-345:

void AutomationEventRouter::BindForRenderer(
    RenderProcessHostId render_process_id,
    mojo::PendingAssociatedReceiver<
        extensions::mojom::RendererAutomationRegistry> receiver) {
  AutomationEventRouter* router = AutomationEventRouter::GetInstance();
  CHECK(router);
  router->receivers_.Add(router, std::move(receiver), render_process_id);
}
void AutomationEventRouter::BindAutomation(
    mojo::PendingAssociatedRemote<ax::mojom::Automation> automation) {
  automation_remote_set_.Add(std::move(automation));   // caller id ignored
}

The sibling proves this is an omission, not the design. EventRouter stores the same context and its handlers consume it — RenderProcessHost::FromID(receivers_.current_context()) (event_router.cc:374) → ProcessMap::Get(...)->Contains(extension_id, process.GetID()) (:411-412) / ChildProcessSecurityPolicy::CanAccessDataForOrigin(...) (:420-422) → receivers_.ReportBadMessage(...) on failure (:447). Automation stores the identical context (:355) and reads it nowhere; a grep of automation_internal/ for CanAccessDataForOrigin|ProcessMap|ReportBadMessage is empty — the missing per-caller check is the sole defense.

Delivery is then an unfiltered broadcast to the process-global set — automation_event_router.cc:253-268:

void AutomationEventRouter::DispatchAccessibilityEvents(...) {
  if (remote_router_) { ...; return; }   // dead: RegisterRemoteRouter has no callers
  for (const auto& remote : automation_remote_set_) {
    remote->DispatchAccessibilityEvents(tree_id, updates, mouse_location, events);
  }
}

listeners_, tree ids, and origin are never consulted; the set is a process-global base::Singleton (:32-37) with no BrowserContext key, so the reach spans profiles in source. The payload (services/accessibility/public/mojom/automation.mojom AXTreeUpdate) carries kName (text), kValue (non-password form-field values — password fields are masked in the accessibility tree), kUrl (cross-site frame URLs), placeholders, and descriptions.

Reproduction

RendererAutomationRegistry is an associated Mojo interface, reachable only through the frame’s AssociatedInterfaceProvider (render_frame_host_impl.cc:10299), not through BrowserInterfaceBroker. Blink’s MojoJS exposes only non-associated bindInterface — there is no way to acquire this interface (or pass the associated Automation remote) from page JavaScript, so no HTML/JS reproducer is constructible. A faithful compromised-renderer reproducer is therefore a browsertest driving the real production path, at the same fidelity Chromium’s own security test uses for the sibling: extension_security_exploit_browsertest.cc’s EventRouterExploitTest binds EventRouter::BindForRenderer(web_process->GetID(), receiver) “as though process were the renderer sending the message” (:1221-1228, :1256-1258), driving a real foo.com web-page process. The attached automation_registry_exploit_browsertest.cc applies that harness to automation, adding no production code and modifying no existing test:

  1. Navigate an attacker tab to evil.com; capture its render process.
  2. Navigate a victim tab to victim.com carrying a marker secret in an accessible node; stand up a normal chrome.automation consumer over it (a stand-in for ChromeVox). Assert the two processes differ (Site Isolation active).
  3. As that compromised renderer, AutomationEventRouter::BindForRenderer(attacker_process_id, …) + BindAutomation(rogue_receiver) — the browser-side-static fidelity above (the routing that delivers a real web frame’s request to this handler is traced verbatim in §Reachability).
  4. Trigger an accessibility event on victim.com.
  5. Assert the rogue receiver, hosted for the evil.com web renderer, receives victim.com’s tree id and the marker secret — with no bad-message / renderer kill (contrast: EventRouterExploitTest asserts a kill for the analogous spoof).

Build/run: add the file to chrome/browser/extensions/BUILD.gn test_sources, then autoninja -C out/Default browser_tests && out/Default/browser_tests --gtest_filter=AutomationRegistryExploitTest.* --single-process-tests.

Suggested remediation

Validate the caller in BindAutomation, mirroring the sibling. The calling render_process_id is already stored as receiver context (automation_event_router.cc:355); retrieve it (RenderProcessHost::FromID(receivers_.current_context())) and reject a process that does not host an extension holding the automation permission — the same authority EventRouter consults (util::CanRendererActOnBehalfOfExtension / ProcessMap) — with receivers_.ReportBadMessage(...). Defense in depth: scope DispatchAccessibilityEvents per-remote against the listeners_ permissions instead of broadcasting to the whole automation_remote_set_.

Scope considerations

  • Realistic impact is ChromeOS-centric. ChromeVox / Select-to-Speak / Switch Access are chrome.automation component extensions routinely active on ChromeOS, so the precondition is satisfied there and the leak spans origins and profiles. On desktop the automation permission is allow-list-restricted, so a consumer must be one of those trusted/component extensions; the code-level crossing is real on all platforms, but the routine real-world exposure is ChromeOS.
  • Primitive is read-only. The ungated interface only lets the attacker receive; the accessibility action/write path (performAction) is a separate permission-gated extension function and is not reachable here.
  • Unconditional low-value signal. Independent of any consumer, the rogue receiver also gets DispatchTreeDestroyedEvent for every document teardown browser-wide (opaque tree ids) — a cross-profile activity-timing oracle. Noted for completeness; the content read above is the substantive issue.

Anticipated objections

  • “Your reproducer calls the browser-side BindForRenderer directly, not a real renderer IPC — does a live web frame actually reach it?” Yes; §Reachability quotes the entire path verbatim: registration runs for every RenderFrameHost with no frame-type/scheme filter (receiver_bindings.cc:400-407), binds RendererAutomationRegistry with the frame’s auto-injected process id (extensions_part_bindings.cc:66-72), and a live frame’s GetAssociatedInterface request reaches the handler through a name lookup with no security gate (render_frame_host_impl.cc:10299-10318) — no intermediate extension-frame/process-type check anywhere. The browser-side-static call is not a shortcut around a check; it is the exact fidelity Chromium’s own security suite uses for the identical interface class (EventRouterExploitTest binds EventRouter::BindForRenderer(web_process->GetID(), receiver) “as though process were the renderer”), applied to the sibling that omits the validation. The runtime PASS then confirms the handler accepts it with no bad-message.
  • “Broadcast-to-all is intended; the automation renderer bindings filter by tree id.” Renderer-side filtering is a functional convenience, not a boundary against a compromised renderer, which keeps every tree; the browser performs no per-remote/origin filter at dispatch. The sibling EventRouter shows the intended enforcement point is browser-side.
  • “Requires a compromised renderer.” That is the premise of the Site Isolation threat model; compromised-renderers.md lists cross-site frame text and URLs as assets the browser must protect from exactly this attacker.

Disclosure

Reported privately to Google only; standard 14-week post-fix disclosure. Happy to adjust the timeline on request.

Impact analysis

Impact analysis

Confidentiality — cross-origin data disclosure (Site Isolation bypass). A single compromised, site-locked renderer gains a persistent read feed of other origins’ serialized accessibility trees: visible page text (kName), non-password form-field values (kValue; password fields are masked in the AX tree), ARIA, and cross-site frame URLs (kUrl). This is exactly the “Text … of cross-site frames” and “Full URL … of cross-site frames” that docs/security/compromised-renderers.md commits to protect from a compromised renderer, and the “sensitive data from the origin [that] should not be shared into unrelated renderer processes” that docs/security/ax-tree-security-guidelines.md forbids passing back down to renderers.

Worst case. While any legitimate chrome.automation consumer is active, the rogue receiver reads the full accessibility tree of every tab that consumer serializes — a broad cross-origin content read from a process Site Isolation is meant to contain. The handler is a process-global singleton with no BrowserContext key, so in source the reach also spans other profiles, including Incognito.

Affected population. The precondition — an active chrome.automation consumer — is routine on ChromeOS, where ChromeVox, Select-to-Speak, and Switch Access are component extensions; so the realistic exposure is ChromeOS-centric. Per the severity rubric, that a bug affects “only … users running with accessibility features enabled” is explicitly not a mitigating factor.

Bounds. The primitive is read-only — the accessibility action/write path (performAction) is a separate permission-gated function and is not reachable here. The cross-profile reach is established at source level; the attached reproducer exercises the cross-origin case within one profile.

Severity. Chrome VRP High per the severity rubric — a Site Isolation bypass / cross-site data disclosure.


The cause

What version of Chrome have you found the security issue in?

152.0.7961.0 (main channel; commit 2a43c44b, 2026-07-28) — verified: the attached browsertest reproducer PASSes on this build against otherwise-pristine production code. The affected code (BindAutomation with no caller check, the RendererAutomationRegistry registration, and the validating EventRouter sibling) is core extensions/accessibility infrastructure enabled by default — no chrome://flags entry, no Finch/experimental gate, not DISABLED_BY_DEFAULT — so it is not a dev-channel-only feature.

No, it is not related to a crash.

Choose the type of vulnerability

Sensitive data exposure

How would you like to be publicly acknowledged for your report?

OGINOME Tomohito

View on issue tracker