Low chrome Memory Corruption 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in DevTools
DescriptionMemory corruption in DevTools
ComponentDevTools
Bug ClassMemory Corruption
Tracker562955705
Fix commitec847bd3bc84 (devtools/devtools-frontend) +293/-21
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
constructor
front_end/models/ai_assistance/AiAgent2.ts
modified
if
front_end/models/ai_assistance/AiAgent2.ts
modified

Files Changed

  • front_end/models/ai_assistance/agents/ContextSelectionAgent.ts
  • front_end/models/ai_assistance/AiAgent2.ts
  • front_end/models/ai_assistance/AiConversation.ts
  • front_end/models/ai_assistance/agents/ContextSelectionAgent.test.ts
  • front_end/models/ai_assistance/AiConversation.test.ts
From ec847bd3bc84f046dfd80e0f25cacc322cf09689 Mon Sep 17 00:00:00 2001
From: Danil Somsikov <[email protected]>
Date: Wed, 23 Sep 2026 06:31:54 -0700
Subject: [PATCH] Prevent context selection agent from accessing data with undefined or opaque origins

In ContextSelectionAgent, require a valid, non-opaque origin before returning network requests, source files, or storage. This prevents sensitive data from leaking when the primary page origin is undefined, such as during uncommitted navigations.

Additionally, update AiAgent2 to derive its default allowedOrigin from originLock, and ensure AiConversation respects any already-established conversation origin when a run starts.

Bug: 562955705
Change-Id: I096585254c94b5020eefc046da123db324de7f21
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/8439719
Reviewed-by: Alina Varkki <[email protected]>
Commit-Queue: Danil Somsikov <[email protected]>
---
diff --git a/front_end/models/ai_assistance/AiAgent2.ts b/front_end/models/ai_assistance/AiAgent2.ts
index 2fd5379..5141182 100644
--- a/front_end/models/ai_assistance/AiAgent2.ts
+++ b/front_end/models/ai_assistance/AiAgent2.ts
@@ -141,7 +141,19 @@
   readonly #declaredTools = new Set<string>();
 
   constructor(opts: AiAgent2Options) {
-    super(opts);
+    super({
+      ...opts,
+      allowedOrigin: opts.allowedOrigin ?? (() => {
+                       const lock = opts.originLock();
+                       if (lock.status === 'BLOCKED_BY_NAVIGATION') {
+                         return {blocked: true};
+                       }
+                       if (lock.status === 'ESTABLISHED_ORIGIN') {
+                         return {origin: lock.origin};
+                       }
+                       return {origin: undefined};
+                     }),
+    });
     this.#changes = opts.changeManager ?? new ChangeManager(opts.targetManager);
     this.#lighthouseRecording = opts.lighthouseRecording;
     this.#performanceRecordAndReload = opts.performanceRecordAndReload;
diff --git a/front_end/models/ai_assistance/AiConversation.test.ts b/front_end/models/ai_assistance/AiConversation.test.ts
index af3354a..d743ced 100644
--- a/front_end/models/ai_assistance/AiConversation.test.ts
+++ b/front_end/models/ai_assistance/AiConversation.test.ts
@@ -298,6 +298,12 @@
 
   it('should update context when agent returns CONTEXT_CHANGE', async () => {
     updateHostConfig({devToolsAiAssistanceContextSelectionAgent: {enabled: true}});
+    const origin = Platform.DevToolsPath.urlString`https://example.com`;
+    const target = sinon.createStubInstance(SDK.Target.Target);
+    target.inspectedURL.returns(Platform.DevToolsPath.urlString`${origin}/`);
+    target.inspectedSecurityOrigin.returns(SDK.SecurityOrigin.SecurityOrigin.create(origin));
+    sinon.stub(universe.targetManager, 'primaryPageTarget').returns(target);
+
     const workspace = universe.workspace;
     const project = {
       id: () => 'test-project',
@@ -381,6 +387,12 @@
   it('should update conversation origin when agent returns CONTEXT_CHANGE', async () => {
     updateHostConfig({devToolsAiAssistanceContextSelectionAgent: {enabled: true}});
 
+    const originUrl = Platform.DevToolsPath.urlString`https://example.com`;
+    const target = sinon.createStubInstance(SDK.Target.Target);
+    target.inspectedURL.returns(Platform.DevToolsPath.urlString`${originUrl}/`);
+    target.inspectedSecurityOrigin.returns(SDK.SecurityOrigin.SecurityOrigin.create(originUrl));
+    sinon.stub(universe.targetManager, 'primaryPageTarget').returns(target);
+
     const aidaClient = mockAidaClient([
       [
         {
@@ -425,9 +437,72 @@
     assert.isTrue(origin.isSameOriginWith(SDK.SecurityOrigin.SecurityOrigin.create('https://example.com')));
   });
 
+  it('does not leak network requests or lock origin when primary page origin is undefined (uncommitted navigation)',
+     async () => {
+       updateHostConfig({devToolsAiAssistanceContextSelectionAgent: {enabled: true}});
+
+       const target = sinon.createStubInstance(SDK.Target.Target);
+       target.inspectedURL.returns(Platform.DevToolsPath.EmptyUrlString);
+       sinon.stub(universe.targetManager, 'primaryPageTarget').returns(target);
+
+       const networkRequest = createNetworkRequest({
+         requestId: 'requestId-0',
+         url: Platform.DevToolsPath.urlString`https://idp.example/auth?saml_assertion=secret`,
+         documentURL: Platform.DevToolsPath.urlString`https://idp.example/login`,
+       });
+       sinon.stub(universe.networkLog, 'requests').returns([networkRequest]);
+
+       const aidaClient = mockAidaClient([
+         [{
+           explanation: '',
+           functionCalls: [{name: 'listNetworkRequests', args: {}}],
+         }],
+         [{
+           explanation: '',
+           functionCalls: [{name: 'selectNetworkRequest', args: {id: 'requestId-0'}}],
+         }],
+         [{explanation: 'Done'}],
+       ]);
+
+       const conversation = new AiAssistance.AiConversation.AiConversation({
+         type: AiAssistance.AiHistoryStorage.ConversationType.NONE,
+         data: [],
+         id: 'test-id',
+         isReadOnly: false,
+         aidaClient,
+       });
+
+       await Array.fromAsync(conversation.run('what requests are slow?'));
+
+       assert.isUndefined(conversation.origin);
+       assert.isUndefined(conversation.selectedContext);
+
+       const listCall = aidaClient.doConversation.getCall(1).firstArg;
+       const listPart = listCall.current_message.parts[0];
+       assert(listPart && 'functionResponse' in listPart);
+       assert.deepEqual(listPart.functionResponse.response, {
+         error: 'No requests recorded by DevTools',
+         widgets: undefined,
+       });
+
+       const selectCall = aidaClient.doConversation.getCall(2).firstArg;
+       const selectPart = selectCall.current_message.parts[0];
+       assert(selectPart && 'functionResponse' in selectPart);
+       assert.deepEqual(selectPart.functionResponse.response, {
+         error: 'No request found',
+         widgets: undefined,
+       });
+     });
+
   it('should forward history to the new agent when switching agents', async () => {
     updateHostConfig({devToolsAiAssistanceContextSelectionAgent: {enabled: true}});
 
+    const originUrl = Platform.DevToolsPath.urlString`https://example.com`;
+    const target = sinon.createStubInstance(SDK.Target.Target);
+    target.inspectedURL.returns(Platform.DevToolsPath.urlString`${originUrl}/`);
+    target.inspectedSecurityOrigin.returns(SDK.SecurityOrigin.SecurityOrigin.create(originUrl));
+    sinon.stub(universe.targetManager, 'primaryPageTarget').returns(target);
+
     function hasFunctionCalls(request: Host.AidaClient.DoConversationRequest): boolean {
       return request.historical_contexts?.some(history => {
         return history.parts.some(part => 'functionCall' in part || 'functionResponse' in part);
diff --git a/front_end/models/ai_assistance/AiConversation.ts b/front_end/models/ai_assistance/AiConversation.ts
index 5a76480..2e9da31 100644
--- a/front_end/models/ai_assistance/AiConversation.ts
+++ b/front_end/models/ai_assistance/AiConversation.ts
@@ -434,7 +434,7 @@
           } = {},
           ): AsyncGenerator<ResponseData, void, void> {
     this.#navigationOccurredDuringRun = false;
-    const originAtRunStart = getPrimaryPageSecurityOrigin(this.#targetManager);
+    const originAtRunStart = this.#origin ?? getPrimaryPageSecurityOrigin(this.#targetManager);
     const listener = (): void => {
       // Prevent the agent from executing tools or reading data from an untrusted origin
       // if the page navigates unexpectedly during execution.
diff --git a/front_end/models/ai_assistance/agents/ContextSelectionAgent.test.ts b/front_end/models/ai_assistance/agents/ContextSelectionAgent.test.ts
index 543dc9a..42508c7 100644
--- a/front_end/models/ai_assistance/agents/ContextSelectionAgent.test.ts
+++ b/front_end/models/ai_assistance/agents/ContextSelectionAgent.test.ts
@@ -191,6 +191,7 @@
           }],
           [{explanation: 'Done'}],
         ]),
+        allowedOrigin: () => ({origin: SDK.SecurityOrigin.SecurityOrigin.create('https://example.com')}),
       });
 
       await Array.fromAsync(agent.run('test', {selected: null}));
@@ -237,6 +238,40 @@
       ]);
     });
 
+    it('fails to list network requests when origin is undefined', async () => {
+      const request = createNetworkRequest({
+        requestId: 'requestId',
+        url: 'https://example.com/secret',
+        documentURL: 'https://example.com/',
+        statusCode: 200,
+      });
+      sinon.stub(universe.networkLog, 'requests').returns([request]);
+
+      const agent = new ContextSelectionAgent.ContextSelectionAgent({
+        aidaClient: mockAidaClient([
+          [{
+            functionCalls: [{
+              name: 'listNetworkRequests',
+              args: {},
+            }],
+            explanation: '',
+          }],
+          [{explanation: 'Done'}],
+        ]),
+        allowedOrigin: () => ({origin: undefined}),
+      });
+
+      await Array.fromAsync(agent.run('test', {selected: null}));
+
+      const requestToAida = agent.buildRequest({text: ''}, Host.AidaClient.Role.USER);
+      const part = requestToAida.historical_contexts?.[2].parts[0];
+      assert(part && 'functionResponse' in part);
+      assert.deepEqual(part.functionResponse.response, {
+        error: 'No requests recorded by DevTools',
+        widgets: undefined,
+      });
+    });
+
     it('fails to list network requests for opaque origins', async () => {
       const agent = new ContextSelectionAgent.ContextSelectionAgent({
         aidaClient: mockAidaClient([
@@ -736,6 +771,7 @@
           }],
           [{explanation: 'Done'}],
         ]),
+        allowedOrigin: () => ({origin: SDK.SecurityOrigin.SecurityOrigin.create('https://example.com')}),
       });
 
       const responses = await Array.fromAsync(agent.run('test', {selected: null}));
@@ -751,6 +787,41 @@
       assert.strictEqual(widget.data.request, request);
     });
 
+    it('fails to select network request when origin is undefined', async () => {
+      const request = createNetworkRequest({
+        requestId: 'requestId',
+        url: 'https://example.com/',
+        documentURL: 'https://example.com/',
+        statusCode: 200,
+      });
+      sinon.stub(universe.networkLog, 'requests').returns([request]);
+
+      const agent = new ContextSelectionAgent.ContextSelectionAgent({
+        aidaClient: mockAidaClient([
+          [{
+            functionCalls: [{
+              name: 'selectNetworkRequest',
+              args: {id: 'requestId'},
+            }],
+            explanation: '',
+          }],
+          [{explanation: 'Done'}],
+        ]),
+        allowedOrigin: () => ({origin: undefined}),
+      });
+
+      const responses = await Array.fromAsync(agent.run('test', {selected: null}));
+      assert.isUndefined(responses.find(r => r.type === AiAgent.ResponseType.CONTEXT_CHANGE));
+
+      const requestToAida = agent.buildRequest({text: ''}, Host.AidaClient.Role.USER);
+      const part = requestToAida.historical_contexts?.[2].parts[0];
+      assert(part && 'functionResponse' in part);
+      assert.deepEqual(part.functionResponse.response, {
+        error: 'No request found',
+        widgets: undefined,
+      });
+    });
+
     it('returns an error when selecting cross-origin network request', async () => {
       const request = createNetworkRequest({
         requestId: 'requestId',
@@ -868,6 +939,7 @@
           }],
           [{explanation: 'Done'}],
         ]),
+        allowedOrigin: () => ({origin: SDK.SecurityOrigin.SecurityOrigin.create('https://example.com')}),
       });
 
       const responses = await Array.fromAsync(agent.run('test', {selected: null}));
@@ -882,6 +954,45 @@
       assert.strictEqual(widget.data.uiSourceCode, file);
     });
 
+    it('fails to select source file when origin is undefined', async () => {
+      const workspace = universe.workspace;
+      const project = {
+        id: () => 'test-project',
+        type: () => Workspace.Workspace.projectTypes.Network,
+        uiSourceCodes: () => [file],
+        fullDisplayName: () => 'script.js',
+      } as unknown as Workspace.Workspace.Project;
+      const file = new Workspace.UISourceCode.UISourceCode(project, urlString`https://example.com/script.js`,
+                                                           Common.ResourceType.resourceTypes.Script);
+      sinon.stub(workspace, 'projects').returns([project]);
+      ContextSelectionAgent.ContextSelectionAgent.uiSourceCodeId.set(file, 1);
+
+      const agent = new ContextSelectionAgent.ContextSelectionAgent({
+        aidaClient: mockAidaClient([
+          [{
+            functionCalls: [{
+              name: 'selectSourceFile',
+              args: {id: 1},
+            }],
+            explanation: '',
+          }],
+          [{explanation: 'Done'}],
+        ]),
+        allowedOrigin: () => ({origin: undefined}),
+      });
+
+      const responses = await Array.fromAsync(agent.run('test', {selected: null}));
+      assert.isUndefined(responses.find(r => r.type === AiAgent.ResponseType.CONTEXT_CHANGE));
+
+      const requestToAida = agent.buildRequest({text: ''}, Host.AidaClient.Role.USER);
+      const part = requestToAida.historical_contexts?.[2].parts[0];
+      assert(part && 'functionResponse' in part);
+      assert.deepEqual(part.functionResponse.response, {
+        error: 'Unable to find file.',
+        widgets: undefined,
+      });
+    });
+
     it('returns an error when selecting cross-origin source file', async () => {
       const workspace = universe.workspace;
       const project = {
@@ -954,6 +1065,7 @@
           }],
           [{explanation: 'Done'}],
         ]),
+        allowedOrigin: () => ({origin: SDK.SecurityOrigin.SecurityOrigin.create('https://example.com')}),
       });
 
       const responses = await Array.fromAsync(agent.run('test', {selected: null}));
@@ -993,6 +1105,47 @@
       });
     });
 
+    it('returns empty list for listSourceFiles when origin is undefined', async () => {
+      const workspace = universe.workspace;
+      const project = {
+        id: () => 'test-project',
+        type: () => Workspace.Workspace.projectTypes.Network,
+        uiSourceCodes: () => [file],
+        fullDisplayName: () => 'script.js',
+      } as unknown as Workspace.Workspace.Project;
+      const file = new Workspace.UISourceCode.UISourceCode(project, urlString`https://example.com/script.js`,
+                                                           Common.ResourceType.resourceTypes.Script);
+      sinon.stub(workspace, 'projects').returns([project]);
+      ContextSelectionAgent.ContextSelectionAgent.uiSourceCodeId.set(file, 1);
+
+      const agent = new ContextSelectionAgent.ContextSelectionAgent({
+        aidaClient: mockAidaClient([
+          [{
+            functionCalls: [{
+              name: 'listSourceFiles',
+              args: {},
+            }],
+            explanation: '',
+          }],
+          [{explanation: 'Done'}],
+        ]),
+        allowedOrigin: () => ({origin: undefined}),
+      });
+
+      const responses = await Array.fromAsync(agent.run('test', {selected: null}));
+      const actionResponse = responses.find(response => response.type === AiAgent.ResponseType.ACTION);
+      assert.exists(actionResponse);
+      assert.isUndefined(actionResponse.widgets);
+
+      const requestToAida = agent.buildRequest({text: ''}, Host.AidaClient.Role.USER);
+      const part = requestToAida.historical_contexts?.[2].parts[0];
+      assert(part && 'functionResponse' in part);
+      assert.deepEqual(part.functionResponse.response, {
+        result: [],
+        widgets: undefined,
+      });
+    });
+
     it('filters source files by origin', async () => {
       const workspace = universe.workspace;
       const project = {
@@ -1095,6 +1248,44 @@
       assert.strictEqual(storageContext.getItem().primaryTargetOrigin, 'https://example.com');
       assert.strictEqual(contextChange.description, 'User selected page storage');
     });
+
+    it('fails to analyzeStorage when origin is undefined or opaque', async () => {
+      updateHostConfig({
+        devToolsAiAssistanceContextSelectionAgent: {
+          enabled: true,
+        },
+        devToolsAiAssistanceStorageAgent: {
+          enabled: true,
+        },
+      });
+
+      for (const origin of [undefined, SDK.SecurityOrigin.SecurityOrigin.create('about:blank')]) {
+        const agent = new ContextSelectionAgent.ContextSelectionAgent({
+          aidaClient: mockAidaClient([
+            [{
+              functionCalls: [{
+                name: 'analyzeStorage',
+                args: {},
+              }],
+              explanation: '',
+            }],
+            [{explanation: 'Done'}],
+          ]),
+          allowedOrigin: () => ({origin}),
+        });
+
+        const responses = await Array.fromAsync(agent.run('test', {selected: null}));
... (file diff truncated)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.