Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in DevTools
DescriptionInappropriate implementation in DevTools
ComponentDevTools
Bug ClassLogic Error
Tracker513783632
Fix commit32a24bb2ba47 (devtools/devtools-frontend) +369/-75
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Files Changed

  • front_end/models/ai_assistance/AiUtils.test.ts
  • front_end/models/ai_assistance/AiUtils.ts
  • front_end/models/ai_assistance/BUILD.gn
  • front_end/models/ai_assistance/agents/AccessibilityAgent.test.ts
From 32a24bb2ba47f11049b1ad57c8be87ee8077650e Mon Sep 17 00:00:00 2001
From: Jack Franklin <[email protected]>
Date: Thu, 28 May 2026 13:03:06 +0100
Subject: [PATCH] AI: Improve cross-origin data leak check in AccessibilityAgent

Replace frame ID check with origin check using documentURL to prevent
data leaks from cross-origin iframes while allowing same-origin iframes.
Handle data URLs by comparing the full URL string, as they cannot be
distinguished by origin alone.

This change also adds missing tests for node ID linkification and data
URLs, and updates test descriptions to use "origin" instead of "frame".

Fixed: 513783632
Change-Id: Ic1ec87f10e159a6d3196b229cf6d29eb5a83daf2
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7878867
Commit-Queue: Kim-Anh Tran <[email protected]>
Reviewed-by: Kim-Anh Tran <[email protected]>
Auto-Submit: Jack Franklin <[email protected]>
---

diff --git a/front_end/models/ai_assistance/AiUtils.test.ts b/front_end/models/ai_assistance/AiUtils.test.ts
new file mode 100644
index 0000000..e175b3c
--- /dev/null
+++ b/front_end/models/ai_assistance/AiUtils.test.ts
@@ -0,0 +1,43 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import * as Platform from '../../core/platform/platform.js';
+
+import * as AiAssistance from './ai_assistance.js';
+
+const {urlString} = Platform.DevToolsPath;
+
+describe('AiUtils', () => {
+  describe('isSameOrigin', () => {
+    it('returns true for identical origins', () => {
+      const url1 = urlString`https://example.com/page1`;
+      const url2 = urlString`https://example.com/page2`;
+      assert.isTrue(AiAssistance.AiUtils.isSameOrigin(url1, url2));
+    });
+
+    it('returns false for different origins', () => {
+      const url1 = urlString`https://example.com`;
+      const url2 = urlString`https://google.com`;
+      assert.isFalse(AiAssistance.AiUtils.isSameOrigin(url1, url2));
+    });
+
+    it('returns true for identical data URLs', () => {
+      const url1 = urlString`data:text/html,hello`;
+      const url2 = urlString`data:text/html,hello`;
+      assert.isTrue(AiAssistance.AiUtils.isSameOrigin(url1, url2));
+    });
+
+    it('returns false for different data URLs', () => {
+      const url1 = urlString`data:text/html,hello`;
+      const url2 = urlString`data:text/html,world`;
+      assert.isFalse(AiAssistance.AiUtils.isSameOrigin(url1, url2));
+    });
+
+    it('returns false if one is data URL and other is not', () => {
+      const url1 = urlString`https://example.com`;
+      const url2 = urlString`data:text/html,hello`;
+      assert.isFalse(AiAssistance.AiUtils.isSameOrigin(url1, url2));
+    });
+  });
+});
diff --git a/front_end/models/ai_assistance/AiUtils.ts b/front_end/models/ai_assistance/AiUtils.ts
index 00e9a99..843bd31 100644
--- a/front_end/models/ai_assistance/AiUtils.ts
+++ b/front_end/models/ai_assistance/AiUtils.ts
@@ -62,3 +62,12 @@
 export function getIconName(): string {
   return isGeminiBranding() ? 'spark' : 'smart-assistant';
 }
+
+export function isSameOrigin(url1: Platform.DevToolsPath.UrlString, url2: Platform.DevToolsPath.UrlString): boolean {
+  if (url1.startsWith('data:') || url2.startsWith('data:')) {
+    return url1 === url2;
+  }
+  const origin1 = Common.ParsedURL.ParsedURL.extractOrigin(url1);
+  const origin2 = Common.ParsedURL.ParsedURL.extractOrigin(url2);
+  return origin1 !== '' && origin1 === origin2;
+}
diff --git a/front_end/models/ai_assistance/BUILD.gn b/front_end/models/ai_assistance/BUILD.gn
index 7ee1723..b1a325c 100644
--- a/front_end/models/ai_assistance/BUILD.gn
+++ b/front_end/models/ai_assistance/BUILD.gn
@@ -111,6 +111,7 @@
     "AgentProject.test.ts",
     "AiConversation.test.ts",
     "AiHistoryStorage.test.ts",
+    "AiUtils.test.ts",
     "BuiltInAi.test.ts",
     "ChangeManager.test.ts",
     "EvaluateAction.test.ts",
diff --git a/front_end/models/ai_assistance/agents/AccessibilityAgent.test.ts b/front_end/models/ai_assistance/agents/AccessibilityAgent.test.ts
index d393215..8d4ee3c 100644
--- a/front_end/models/ai_assistance/agents/AccessibilityAgent.test.ts
+++ b/front_end/models/ai_assistance/agents/AccessibilityAgent.test.ts
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import * as Platform from '../../../core/platform/platform.js';
 import * as SDK from '../../../core/sdk/sdk.js';
 import type * as Protocol from '../../../generated/protocol.js';
 import {mockAidaClient} from '../../../testing/AiAssistanceHelpers.js';
@@ -10,6 +11,8 @@
 import type * as LHModel from '../../lighthouse/lighthouse.js';
 import * as AiAssistance from '../ai_assistance.js';
 
+const {urlString} = Platform.DevToolsPath;
+
 describeWithMockConnection('AccessibilityAgent', () => {
   const mockReport = {
     lighthouseVersion: '1.0.0',
@@ -134,62 +137,7 @@
     assert.strictEqual(titleResponse.title, 'Reading accessibility details');
   });
 
-  it('getElementAccessibilityDetails yields a DomTreeAiWidget containing the node snapshot',
-     async () => {
-       const target = createTarget();
-       const aidaClient = mockAidaClient([[{
-         explanation: '',
-         functionCalls:
-             [{name: 'getElementAccessibilityDetails', args: {path: '1,HTML,1,BODY', explanation: 'testing'}}],
-         metadata: {
-           rpcGlobalId: 123,
-         },
-       }]]);
-       const agent = new AiAssistance.AccessibilityAgent.AccessibilityAgent({
-         aidaClient,
-       });
-       const context = new AiAssistance.AccessibilityAgent.AccessibilityContext(mockReport);
-
-       const domModel = target.model(SDK.DOMModel.DOMModel)!;
-       const accessibilityModel = target.model(SDK.AccessibilityModel.AccessibilityModel)!;
-       const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel)!;
-
-       const mockNode = sinon.createStubInstance(SDK.DOMModel.DOMNode);
-       mockNode.domModel.returns(domModel);
-       mockNode.id = 42 as Protocol.DOM.NodeId;
-       mockNode.backendNodeId.returns(100 as Protocol.DOM.BackendNodeId);
-       mockNode.attributes.returns([]);
-       mockNode.frameId.returns('main' as Protocol.Page.FrameId);
-
-       const mockSnapshot = sinon.createStubInstance(SDK.DOMModel.DOMNodeSnapshot);
-       mockNode.takeSnapshot.resolves(mockSnapshot);
-
-       sinon.stub(domModel, 'pushNodeByPathToFrontend').resolves(42 as Protocol.DOM.NodeId);
-       sinon.stub(domModel, 'nodeForId').withArgs(42 as Protocol.DOM.NodeId).returns(mockNode);
-       resourceTreeModel.mainFrame = {id: 'main' as Protocol.Page.FrameId} as SDK.ResourceTreeModel.ResourceTreeFrame;
-
-       sinon.stub(accessibilityModel, 'requestAndLoadSubTreeToNode').resolves();
-       const mockAxNode = sinon.createStubInstance(SDK.AccessibilityModel.AccessibilityNode);
-       mockAxNode.role.returns({value: 'button', type: 'role' as Protocol.Accessibility.AXValueType});
-       mockAxNode.name.returns({
-         value: 'Click me',
-         type: 'string' as Protocol.Accessibility.AXValueType,
-         sources: [{type: 'attribute' as Protocol.Accessibility.AXValueSourceType}],
-       });
-       mockAxNode.ignored.returns(false);
-       mockAxNode.ignoredReasons.returns([]);
-       sinon.stub(accessibilityModel, 'axNodeForDOMNode').withArgs(mockNode).returns(mockAxNode);
-
-       const responses = await Array.fromAsync(agent.run('test', {selected: context}));
-       const actions = responses.filter(r => r.type === AiAssistance.AiAgent.ResponseType.ACTION);
-       assert.lengthOf(actions, 1);
-       assert.exists(actions[0].widgets);
-       const widget = actions[0].widgets?.find(w => w.name === 'DOM_TREE') as AiAssistance.AiAgent.DomTreeAiWidget;
-       assert.exists(widget);
-       assert.strictEqual(widget.data.root, mockSnapshot);
-     });
-
-  it('getElementAccessibilityDetails returns an error if the node is in a different frame', async () => {
+  it('getElementAccessibilityDetails yields a DomTreeAiWidget containing the node snapshot', async () => {
     const target = createTarget();
     const aidaClient = mockAidaClient([[{
       explanation: '',
@@ -204,16 +152,152 @@
     const context = new AiAssistance.AccessibilityAgent.AccessibilityContext(mockReport);
 
     const domModel = target.model(SDK.DOMModel.DOMModel)!;
-    const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel)!;
+    const accessibilityModel = target.model(SDK.AccessibilityModel.AccessibilityModel)!;
+    const mockNode = sinon.createStubInstance(SDK.DOMModel.DOMNode);
+    mockNode.domModel.returns(domModel);
+    mockNode.id = 42 as Protocol.DOM.NodeId;
+    mockNode.backendNodeId.returns(100 as Protocol.DOM.BackendNodeId);
+    mockNode.attributes.returns([]);
+    const mockDocument = sinon.createStubInstance(SDK.DOMModel.DOMDocument);
+    mockDocument.documentURL = urlString`https://example.com`;
+    mockNode.ownerDocument = mockDocument;
+
+    const mockSnapshot = sinon.createStubInstance(SDK.DOMModel.DOMNodeSnapshot);
Loading diff…

Original Bug Report

reported by [email protected]

SOP bypass via cross-origin information disclosure in DevTools AI Accessibility Agent

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: The DevTools AI Accessibility Agent resolves DOM paths without verifying frame boundaries, potentially allowing it to access in-process cross-origin iframes. This could enable an attacker to exfiltrate sensitive data from another origin using prompt injection.

Affected files:

  • third_party/devtools-frontend/src/front_end/models/ai_assistance/agents/AccessibilityAgent.ts
  • third_party/devtools-frontend/src/front_end/panels/ai_assistance/components/AccessibilityAgentMarkdownRenderer.ts
  • third_party/devtools-frontend/src/front_end/models/ai_assistance/data_formatters/LighthouseFormatter.ts
  • third_party/blink/renderer/core/inspector/inspector_dom_agent.cc
  • chrome/browser/devtools/features.cc

Estimated timestamp from git blame: 2026-03-19

Summary

A potential vulnerability in the DevTools AI Accessibility Agent allows for cross-origin information disclosure. The agent resolves Lighthouse-style DOM paths provided by the LLM without verifying if the resolved node resides within the expected frame boundary. By utilizing path segments that traverse into child frames, an attacker could potentially use prompt injection to trick the assistant into reading and exfiltrating data from in-process cross-origin iframes.

Root Cause Analysis

The issue exists in AccessibilityAgent.#resolvePathToNode in third_party/devtools-frontend/src/front_end/models/ai_assistance/agents/AccessibilityAgent.ts. It uses domModel.pushNodeByPathToFrontend(path) to resolve a node path:

// front_end/models/ai_assistance/agents/AccessibilityAgent.ts
async #resolvePathToNode(path: string): Promise<SDK.DOMModel.DOMNode|null> {
  // ...
  const nodeId = await domModel.pushNodeByPathToFrontend(path);
  if (!nodeId) { return null; }
  return domModel.nodeForId(nodeId); // Lacks verification of node.frameId()
}

The underlying Chrome DevTools Protocol (CDP) implementation in third_party/blink/renderer/core/inspector/inspector_dom_agent.cc supports traversing into child iframes via the d (or d,#document) path segment. If an iframe is in-process (a common scenario for same-site cross-origin frames), DocumentForFrameOwner returns the child Document object, allowing path resolution to cross origin boundaries.

Tools such as getStyles and getElementAccessibilityDetails consume the resolved node and return sensitive data—such as accessible names (which can include rendered text content), computed styles, and ARIA attributes—to the AI model. If an attacker controls the path argument through prompt injection, they can exfiltrate this data.

This appears to be an oversight, as similar logic in the same module (AccessibilityAgentMarkdownRenderer.ts) correctly enforces the frame boundary by checking node.frameId() !== this.mainFrameId before linkifying a node.

Potential Attack Vector

An attacker could potentially trigger this vulnerability using prompt injection:

  1. Preparation: Host a page at https://attacker.example.com that embeds a same-site cross-origin iframe from https://victim.example.com.
  2. Injection: Include a DOM element on the attacker page that intentionally fails a Lighthouse accessibility audit. Place a prompt injection payload in a field that Lighthouse extracts (e.g., a title or aria-label). The payload would instruct the LLM to call getElementAccessibilityDetails on a path targeting the victim iframe (e.g., 1,HTML,1,BODY,0,IFRAME,d,#document,1,HTML,1,BODY,0,H1).
  3. Trigger: A developer runs a Lighthouse audit on the attacker’s page and then uses the AI Accessibility Agent to investigate the report.
  4. Exfiltration: The LLM, following the injected instructions, fetches the cross-origin data and could be further instructed to exfiltrate it via the executeJavaScript tool.

Impact

Successful exploitation could lead to a Same-Origin Policy (SOP) bypass, allowing the disclosure of sensitive rendered text, CSS properties, and ARIA attributes from cross-origin in-process iframes.

Suggested Fix

Modify AccessibilityAgent.#resolvePathToNode to verify that the resolved node belongs to the main frame of the page being inspected. The main frame ID can be retrieved from the ResourceTreeModel and compared against node.frameId().

const resourceTreeModel = domModel.target().model(SDK.ResourceTreeModel.ResourceTreeModel);
const mainFrameId = resourceTreeModel?.mainFrame?.id;
if (node.frameId() !== mainFrameId) {
  return null;
}

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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