Medium watch CVSS 3.1 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in DevTools
DescriptionInsufficient validation of untrusted input in DevTools
ComponentDevTools
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker497255035
Fix commita77ddcdbea95 (chromium/src) +8/-12
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-28 (per vendor advisory)
Fix landed2026-04-02 (public git)
Disclosed2026-05-05
Patch gap33 days — fix public in source before the advisory

Background

`devtools_frame_token`
a per-frame token that identifies a frame to the DevTools protocol; a DevToolsAgentHost is registered under this token in a process-global map.
Fenced frame
an embedded frame type whose creation is requested from the renderer and processed by RenderFrameHostImpl (the hunk sits in the fenced-frame creation path in render_frame_host_impl.cc).
`DevToolsAgentHost::GetForId`
a lookup that returns the DevToolsAgentHost registered globally for a given id string, i.e. across all tabs/instances rather than one frame tree.
`DevToolsAgentHostImpl::NotifyCreated`
the registration step that inserts this into the global DevTools instances map keyed by id_.

Root Cause Analysis

When a fenced frame was created, the browser validated the supplied devtools_frame_token for uniqueness only within the current tab, by iterating GetOutermostMainFrame()->frame_tree()->NodesIncludingInnerTreeNodes() and comparing each node’s devtools_frame_token(). The DevTools target map, however, is process-global, so a token that was already in use by a cross-origin page in another tab passed the per-tab check. NotifyCreated then inserted into the global map under a DCHECK (compiled out in release), so on collision it silently overwrote the existing DevToolsAgentHost entry, allowing a target hijack. The invariant that each devtools_frame_token maps to exactly one DevTools target globally was not enforced.

The fix replaces the per-tab loop with a global lookup DevToolsAgentHost::GetForId(devtools_frame_token.ToString()); if any host already exists for that token, it is rejected as a bad message and the renderer is terminated. As defense in depth, NotifyCreated’s DCHECK is upgraded to a CHECK so a bypassing collision crashes safely instead of silently overwriting.

Key insight
The uniqueness check was scoped to the current tab while the DevTools target map is process-global; the fix validates global uniqueness via GetForId (bad-message/kill on collision) and hardens the map insert with a CHECK.

Attack Path

  1. Compromise a renderer Obtain a renderer that can issue the fenced-frame creation IPC with an attacker-chosen devtools_frame_token.
  2. Supply a stolen token Provide a devtools_frame_token already associated with a cross-origin page whose DevTools target lives in another tab/instance.
  3. Evade per-tab validation The old check only scanned the current tab’s frame tree, so a token in use elsewhere was not detected and creation proceeded.
  4. Overwrite the global target map NotifyCreated inserted under the colliding id, silently replacing the existing DevToolsAgentHost (the DCHECK did nothing in release), hijacking the DevTools session for the cross-origin target (inferred: cross-origin DevTools control/information disclosure).

Impact Assessment

A compromised renderer could hijack a DevTools session belonging to a cross-origin page by reusing that page’s devtools_frame_token during fenced-frame creation, gaining DevTools-level influence over a target it should not control (inferred, scoped to what the commit message describes as “target confusion”). The context is the browser process’s fenced-frame creation path, and the precondition is a compromised renderer able to send the creation IPC. The advisory rates this medium and classifies it as “Insufficient validation of untrusted input in DevTools”; no in-the-wild exploitation is stated.

Files Changed

  • content/browser/devtools/devtools_agent_host_impl.cc
  • content/browser/renderer_host/render_frame_host_impl.cc

Audit Directions

  • Scope mismatch between check and store
    Look for uniqueness/validation checks scoped to a single frame tree or tab while the underlying registry is process-global (or vice-versa), especially for renderer-supplied identifiers.
  • Release-stripped assertions guarding security invariants
    Audit DCHECKs that protect map inserts or overwrites of security-relevant state; these do nothing in production and should be CHECKs or explicit error handling.
  • Renderer-controlled tokens
    Trace other renderer-supplied frame/target tokens (DevTools, portals, prerender) to confirm the browser enforces global uniqueness before trusting them as map keys.
From a77ddcdbea95bd88b0534a0d33fcc7f2e30ab4a2 Mon Sep 17 00:00:00 2001
From: Danil Somsikov <dsv@chromium.org>
Date: Thu, 02 Apr 2026 05:00:50 -0700
Subject: [PATCH] Fix DevTools Target Confusion vulnerability via Fenced Frames

A compromised renderer can hijack DevTools sessions for cross-origin
pages by supplying a stolen 'devtools_frame_token' during fenced frame
creation. Previously, the browser improperly scoped token uniqueness
validation to the current tab and silently overwrote the process-global
DevTools target map on collision.

This CL mitigates the vulnerability by:

1. Updating the validation in to check for global uniqueness across all
DevTools instances using `DevToolsAgentHost::GetForId()`. If a collision
is detected, the browser will now treat it as a Bad Message and
terminate the malicious renderer process.

2. Hardening the map insertion logic in
`DevToolsAgentHostImpl::NotifyCreated` by upgrading a `DCHECK` to a
`CHECK`. This ensures that even if a collision somehow bypasses initial
validation, the browser will crash safely in production rather than
allowing a silent target hijack.

Bug: 497255035
Change-Id: I9857e1378ab289be626e71aa12944ef8c9e9cb2f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7716666
Auto-Submit: Danil Somsikov <dsv@chromium.org>
Reviewed-by: Andrey Kosyakov <caseq@chromium.org>
Reviewed-by: Arthur Sonzogni <arthursonzogni@chromium.org>
Commit-Queue: Danil Somsikov <dsv@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1609111}
---

diff --git a/content/browser/devtools/devtools_agent_host_impl.cc b/content/browser/devtools/devtools_agent_host_impl.cc
index e47209e..d7c727ed 100644
--- a/content/browser/devtools/devtools_agent_host_impl.cc
+++ b/content/browser/devtools/devtools_agent_host_impl.cc
@@ -556,7 +556,7 @@
 }
 
 void DevToolsAgentHostImpl::NotifyCreated() {
-  DCHECK(!GetDevtoolsInstances().contains(id_));
+  CHECK(!GetDevtoolsInstances().contains(id_));
   GetDevtoolsInstances()[id_] = this;
   for (auto& observer : GetDevtoolsObservers())
     observer.DevToolsAgentHostCreated(this);
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 3f5964a6..cb6aa69 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -200,6 +200,7 @@
 #include "content/public/browser/content_browser_client.h"
 #include "content/public/browser/context_menu_params.h"
 #include "content/public/browser/cookie_access_details.h"
+#include "content/public/browser/devtools_agent_host.h"
 #include "content/public/browser/disallow_activation_reason.h"
 #include "content/public/browser/document_ref.h"
 #include "content/public/browser/document_service_internal.h"
@@ -10586,17 +10587,12 @@
     return;
   }
 
-  // Ensure the devtools frame token doesn't exist in the FrameTree for
-  // this tab.
-  for (FrameTreeNode* node :
-       GetOutermostMainFrame()->frame_tree()->NodesIncludingInnerTreeNodes()) {
-    if (node->current_frame_host()->devtools_frame_token() ==
-        devtools_frame_token) {
-      bad_message::ReceivedBadMessage(
-          GetProcess(),
-          bad_message::RFHI_CREATE_FENCED_FRAME_BAD_DEVTOOLS_FRAME_TOKEN);
-      return;
-    }
+  // Ensure the devtools frame token doesn't exist globally.
+  if (DevToolsAgentHost::GetForId(devtools_frame_token.ToString())) {
+    bad_message::ReceivedBadMessage(
+        GetProcess(),
+        bad_message::RFHI_CREATE_FENCED_FRAME_BAD_DEVTOOLS_FRAME_TOKEN);
+    return;
   }
 
   // Inactive pages cannot create fenced frames. If the page is in the BFCache,
Loading diff…

Original Bug Report

reported by vm...@google.com

DevTools Target Confusion via Fenced Frame Token Collision

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A compromised renderer can hijack DevTools sessions for cross-origin pages by supplying a stolen devtools_frame_token during fenced frame creation. The browser improperly scopes token uniqueness validation to the current tab and silently overwrites the process-global DevTools target map on collision, allowing an attacker to intercept Chrome DevTools Protocol (CDP) commands.

Affected files:

  • content/browser/devtools/devtools_agent_host_impl.cc
  • content/browser/devtools/render_frame_devtools_agent_host.cc
  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/renderer_host/render_frame_proxy_host.cc

Estimated timestamp from git blame: 2026-01-06

Overview

A logic error exists in DevToolsAgentHostImpl where process-global DevTools target IDs can be overwritten due to insufficient validation during Fenced Frame creation. A compromised renderer can exploit this to hijack DevTools automation sessions (e.g., Puppeteer) connecting to cross-origin targets, leading to cross-origin data theft.

Vulnerability Details

The issue stems from a combination of three factors:

  1. Cross-WebContents Token Leak: When a renderer opens a cross-origin popup, the browser sends a PageBroadcast.CreateRemoteMainFrame Mojo IPC to the opener renderer so it can build a remote frame proxy. This IPC leaks the victim frame’s devtools_frame_token to the potentially compromised opener renderer.
  2. Insufficient Validation Scope: When a renderer creates a Fenced Frame, it supplies a devtools_frame_token via the LocalFrameHost.CreateFencedFrame Mojo IPC. RenderFrameHostImpl::CreateFencedFrame attempts to ensure this token is unique to prevent collisions. However, it only checks nodes within the current tab’s FrameTree (GetOutermostMainFrame()->frame_tree()->NodesIncludingInnerTreeNodes()). It fails to check for collisions across different WebContents.
  3. Silent Map Overwrite: When the DevTools agent host is created for the new fenced frame, DevToolsAgentHostImpl::NotifyCreated registers it in a process-global map:
    void DevToolsAgentHostImpl::NotifyCreated() {
      DCHECK(!GetDevtoolsInstances().contains(id_));
      GetDevtoolsInstances()[id_] = this;
      // ...
    }
    
    Because DCHECK is compiled out in Release builds, the global map entry for the victim’s token is silently overwritten with a pointer to the attacker’s new fenced frame agent host.

Potential Attack Steps

Note: These are theoretical steps as our tooling does not yet have the ability to run code to produce a working proof-of-concept.

  1. An attacker gains code execution in a renderer process (e.g., via a V8 bug).
  2. The compromised renderer opens a cross-origin window to a sensitive site (e.g., window.open('https://victim.com')).
  3. The browser sends a CreateRemoteMainFrame IPC to the attacker’s renderer. The attacker intercepts this IPC and extracts the victim’s devtools_frame_token.
  4. The compromised renderer crafts a LocalFrameHost.CreateFencedFrame Mojo IPC to create a fenced frame in its own document, passing the stolen victim’s devtools_frame_token instead of a randomly generated one.
  5. The browser’s validation passes because the victim is in a different WebContents.
  6. The browser creates a RenderFrameDevToolsAgentHost for the attacker’s fenced frame, which silently overwrites the victim’s entry in the global GetDevtoolsInstances() map.
  7. An automated DevTools client (like Puppeteer or a Chrome Extension) attempts to attach to the victim’s target ID.
  8. DevToolsAgentHostImpl::GetForId returns the attacker’s fenced frame. The DevTools client unknowingly sends CDP commands (e.g., Runtime.evaluate to inject scripts or Input.dispatchKeyEvent to type passwords) into the attacker’s frame, allowing the attacker to steal credentials or hijack the session.

Suggested Fix

  1. Harden Map Insertion: In DevToolsAgentHostImpl::NotifyCreated(), upgrade the DCHECK to a CHECK, or safely handle the collision without overwriting existing valid instances.
  2. Enforce Global Uniqueness: Update the validation in RenderFrameHostImpl::CreateFencedFrame to check the global DevToolsAgentHost::GetForId() (or a truly global registry) to ensure the token doesn’t collide with any existing target across all WebContents.
  3. Browser-Side Token Generation: Consider having the browser unconditionally generate the devtools_frame_token for Fenced Frames rather than trusting the token provided by the renderer via IPC.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.

View on issue tracker
Links in the report