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
Tracker513727626
Fix commit281217b2c13d (devtools/devtools-frontend) +207/-33
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
front_end/models/ai_assistance/AiConversation.test.ts
modified
if
front_end/models/ai_assistance/AiConversation.ts
modified

Files Changed

  • front_end/models/ai_assistance/AiConversation.test.ts
  • front_end/models/ai_assistance/AiConversation.ts
From 281217b2c13d7f0ded40072b6082d6fe18277ab8 Mon Sep 17 00:00:00 2001
From: Kim-Anh Tran <[email protected]>
Date: Tue, 19 May 2026 16:44:23 +0200
Subject: [PATCH] Disallow function call on navigation

When the ContextSelectionAgent is active we don't have a context
and thus no 'locked' origin. It may happen that the user asks a
question and the page is navigated to a different page, ultimately
answering the question on the navigated-to page, at which point the
origin gets locked (if a function was called).

Since this may be unwanted, we listen to page navigated events and
set `allowedOrigin` to `{blocked: true}`. This will result in an
error, disallowing the agent to continue with the current tool call.

Bug: 513727626
Change-Id: I35cab5643e475573df4965b6446f0360ece16798
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7852717
Reviewed-by: Nikolay Vitkov <[email protected]>
Reviewed-by: Jack Franklin <[email protected]>
Commit-Queue: Kim-Anh Tran <[email protected]>
---

diff --git a/front_end/models/ai_assistance/AiConversation.test.ts b/front_end/models/ai_assistance/AiConversation.test.ts
index 0670664..6b23d61 100644
--- a/front_end/models/ai_assistance/AiConversation.test.ts
+++ b/front_end/models/ai_assistance/AiConversation.test.ts
@@ -10,6 +10,7 @@
 import * as TextUtils from '../../models/text_utils/text_utils.js';
 import {createNetworkRequest, mockAidaClient} from '../../testing/AiAssistanceHelpers.js';
 import {
+  createTarget,
   describeWithEnvironment,
   updateHostConfig,
 } from '../../testing/EnvironmentHelpers.js';
@@ -472,4 +473,108 @@
     assert.strictEqual(serialized.history[2].type, AiAssistance.AiAgent.ResponseType.ACTION);
     assert.isUndefined((serialized.history[2] as AiAssistance.AiAgent.ActionResponse).widgets);
   });
+
+  async function testNavigationDuringRun({
+    navigationUrl,
+    expectBlocked,
+  }: {
+    navigationUrl: Platform.DevToolsPath.UrlString,
+    expectBlocked: boolean,
+  }) {
+    updateHostConfig({devToolsAiAssistanceContextSelectionAgent: {enabled: true}});
+
+    const origin = Platform.DevToolsPath.urlString`https://example.com`;
+
+    const target = createTarget({url: Platform.DevToolsPath.urlString`${origin}/`});
+    target.setInspectedURL(Platform.DevToolsPath.urlString`${origin}/`);
+
+    const request = SDK.NetworkRequest.NetworkRequest.create(
+        'requestId1' as Protocol.Network.RequestId,
+        Platform.DevToolsPath.urlString`${origin}/foo`,
+        Platform.DevToolsPath.urlString`${origin}/foo`,
+        null,
+        null,
+        null,
+    );
+    request.statusCode = 200;
+    request.setIssueTime(0, 0);
+    request.endTime = 1;
+
+    const networkLog = Logs.NetworkLog.NetworkLog.instance();
+    sinon.stub(networkLog, 'requests').returns([request]);
+
+    const aidaClient = mockAidaClient([
+      [{
+        functionCalls: [{
+          name: 'listNetworkRequests',
+          args: {},
+        }],
+        explanation: '',
+      }],
+      [{explanation: 'Done'}],
+    ]);
+    const conversation = new AiAssistance.AiConversation.AiConversation({
+      type: AiAssistance.AiHistoryStorage.ConversationType.NONE,
+      data: [],
+      id: 'test-id',
+      isReadOnly: false,
+      aidaClient,
+    });
+
+    const generator = conversation.run('test');
+
+    // First yield should be the UserQuery
+    const firstYield = await generator.next();
+    assert.strictEqual(firstYield.value?.type, AiAssistance.AiAgent.ResponseType.USER_QUERY);
+
+    // Simulate navigation BEFORE the tool call is processed
+    target.setInspectedURL(navigationUrl);
+    const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
+    assert.exists(resourceTreeModel);
+    resourceTreeModel.dispatchEventToListeners(SDK.ResourceTreeModel.Events.PrimaryPageChanged, {
+      frame: {
+        resourceTreeModel: () => resourceTreeModel,
+        unreachableUrl: () => '',
+      } as unknown as SDK.ResourceTreeModel.ResourceTreeFrame,
+      type: SDK.ResourceTreeModel.PrimaryPageChangeType.NAVIGATION
+    });
+
+    // Continue running the generator to completion
+    const results = [];
+    for await (const result of generator) {
+      results.push(result);
+    }
+
+    const errorResult = results.find(r => r.type === AiAssistance.AiAgent.ResponseType.ERROR);
+    if (expectBlocked) {
+      assert.exists(errorResult);
+      assert.strictEqual(errorResult.error, AiAssistance.AiAgent.ErrorType.CROSS_ORIGIN);
+      sinon.assert.callCount(aidaClient.doConversation, 1);
+    } else {
+      assert.isUndefined(errorResult);
+      sinon.assert.callCount(aidaClient.doConversation, 2);
+    }
+  }
+
+  it('blocks tool calls if navigation occurs during the run', async () => {
+    const otherOrigin = Platform.DevToolsPath.urlString`https://other.com`;
+    await testNavigationDuringRun({
+      navigationUrl: Platform.DevToolsPath.urlString`${otherOrigin}/`,
+      expectBlocked: true,
+    });
+  });
+
+  it('does NOT block tool calls if navigation is to about://', async () => {
+    await testNavigationDuringRun({
+      navigationUrl: Platform.DevToolsPath.urlString`about://`,
+      expectBlocked: false,
+    });
+  });
+
+  it('does NOT block tool calls if navigation is to chrome://terms', async () => {
+    await testNavigationDuringRun({
+      navigationUrl: Platform.DevToolsPath.urlString`chrome://terms`,
+      expectBlocked: false,
+    });
+  });
 });
diff --git a/front_end/models/ai_assistance/AiConversation.ts b/front_end/models/ai_assistance/AiConversation.ts
index addf3ca..f383549 100644
--- a/front_end/models/ai_assistance/AiConversation.ts
+++ b/front_end/models/ai_assistance/AiConversation.ts
@@ -14,6 +14,7 @@
 import {AccessibilityAgent, AccessibilityContext} from './agents/AccessibilityAgent.js';
 import {
   type AiAgent,
+  type AllowedOriginResult,
   type ContextDetail,
   type ConversationContext,
   ErrorType,
@@ -34,6 +35,17 @@
 export const NOT_FOUND_IMAGE_DATA = '';
 export const CONTEXT_TITLE = 'Analyzing data';
 const MAX_TITLE_LENGTH = 80;
+/**
+ * List of page navigations that are allowed during an AI agent run.
+ * These are page navigations triggered by agents themselves:
+ * - `about://` : Navigated to before initiating a trace recording to ensure a clean state.
+ * - `chrome://terms`: Navigated to by Lighthouse during its Back-Forward Cache
+ *    audit.
+ */
+export const ALLOWED_PAGE_NAVIGATIONS: Platform.DevToolsPath.UrlString[] = [
+  Platform.DevToolsPath.urlString`about://`,
+  Platform.DevToolsPath.urlString`chrome://terms`,
+];
 
 export function generateContextDetailsMarkdown(details: ContextDetail[]): string {
   const detailsMarkdown: string[] = [];
@@ -87,6 +99,7 @@
   #aidaClient: Host.AidaClient.AidaClient;
   #changeManager: ChangeManager|undefined;
   #origin?: string;
+  #navigationOccurredDuringRun = false;
 
   #contexts: Array<ConversationContext<unknown>> = [];
 
@@ -386,22 +399,43 @@
             multimodalInput?: MultimodalInput,
           } = {},
           ): AsyncGenerator<ResponseData, void, void> {
-    if (this.isBlockedByOrigin) {
-      // This error should not be reached. If it happens, some
-      // invariants do not hold anymore.
-      throw new Error('cross-origin context data should not be included');
-    }
-
-    const userQuery: UserQuery = {
-      type: ResponseType.USER_QUERY,
-      query: initialQuery,
-      imageInput: options.multimodalInput?.input,
-      imageId: options.multimodalInput?.id,
+    this.#navigationOccurredDuringRun = false;
+    const originAtRunStart = getPrimaryPageOrigin();
+    const listener = (): void => {
Loading diff…

Original Bug Report

reported by [email protected]

Potential TOCTOU in DevTools AI Assistance origin lock allows cross-origin data disclosure

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: A potential Time-of-Check Time-of-Use (TOCTOU) vulnerability in DevTools AI Assistance allows an attacker to bypass origin-locking. By navigating the inspected page while an AI query is in flight, the AI can be tricked into locking its security boundary onto a victim origin, leading to the unauthorized disclosure of the victim’s network and source data to the remote AI service.

Affected files:

  • third_party/devtools-frontend/src/front_end/models/ai_assistance/AiConversation.ts
  • third_party/devtools-frontend/src/front_end/models/ai_assistance/agents/ContextSelectionAgent.ts
  • third_party/devtools-frontend/src/front_end/models/ai_assistance/agents/AiAgent.ts
  • third_party/devtools-frontend/src/front_end/models/ai_assistance/agents/NetworkAgent.ts
  • third_party/devtools-frontend/src/front_end/models/ai_assistance/agents/FileAgent.ts
  • third_party/devtools-frontend/src/front_end/panels/ai_assistance/AiAssistancePanel.ts
  • third_party/devtools-frontend/src/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.ts

Estimated timestamp from git blame: Unknown (Google3 checkout)

Summary

The DevTools AI Assistance feature employs an origin-locking mechanism intended to ensure that an AI conversation only accesses data from the origin where the chat was initiated. However, a potential Time-of-Check Time-of-Use (TOCTOU) flaw exists in how this origin is established. For conversations initiated without an explicit initial context (using the ContextSelectionAgent), the authorized origin is lazily initialized only when a tool is first invoked by the LLM, rather than at the moment the user submits their query.

Root Cause Analysis

The vulnerability resides in AiConversation.allowedOrigin() within third_party/devtools-frontend/src/front_end/models/ai_assistance/AiConversation.ts. In a context-free chat session, the #origin property is initially undefined. When the LLM (Aida) decides to call a tool (e.g., listNetworkRequests), it triggers the allowedOrigin() callback to determine the authorized scope.

// third_party/devtools-frontend/src/front_end/models/ai_assistance/AiConversation.ts
allowedOrigin = (): string|undefined => {
  if (this.#origin) {
    return this.#origin;
  }
  const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
  const inspectedURL = target?.inspectedURL();
  this.#origin = inspectedURL ? new Common.ParsedURL.ParsedURL(inspectedURL).securityOrigin() : undefined;
  return this.#origin;
};

Because there is a significant asynchronous delay between the user submitting a query and the LLM responding with a tool call, a malicious page can initiate a navigation to a victim origin (e.g., https://victim.example) during this interval. When the tool handler eventually runs and calls allowedOrigin(), it retrieves the current inspectedURL(), which is now the victim’s URL, and caches it as the conversation’s permanent lock. This allows the AI to filter and collect data (network requests, response bodies, source files) from the victim origin.

Impact

An attacker can cause the AI Assistance feature to exfiltrate sensitive data from a cross-origin victim site to the remote Aida service. This data includes:

  • Network Request Metadata: Full URLs and timing information.
  • Response Bodies: Up to 10,000 characters of unredacted response content (JSON, HTML, etc.).
  • Source Code: Filenames and contents of source files.

While the data is disclosed to the Aida endpoint and not directly to the attacker’s web origin, this constitutes a bypass of the origin-lock security boundary designed to prevent unauthorized cross-origin data processing.

Potential Reproduction Steps

  1. Open DevTools on an attacker-controlled page (e.g., https://attacker.example).
  2. Open the AI Assistance panel and ensure it has no selected element or request.
  3. Type a query such as “What are the network requests on this page?”.
  4. Immediately after submitting, the attacker page executes a navigation (e.g., window.location.href = 'https://victim.example').
  5. The LLM processes the request and returns a call to listNetworkRequests.
  6. The ContextSelectionAgent invokes allowedOrigin(), which finds the victim’s URL and locks the conversation to https://victim.example.
  7. The AI successfully collects and transmits network request details from the victim origin.

Suggested Fix

Initialize the #origin in AiConversation immediately when the conversation is created or at the start of the run() method, using the origin of the primary page target at that specific moment. This ensures the lock is established before any asynchronous processing or potential navigation occurs.

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