Chrome · DevTools
CVE-2026-113254
Memory Corruption in DevTools
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
constructorfront_end/models/ai_assistance/AiAgent2.ts |
modified | |
iffront_end/models/ai_assistance/AiAgent2.ts |
modified |
Files Changed
front_end/models/ai_assistance/agents/ContextSelectionAgent.tsfront_end/models/ai_assistance/AiAgent2.tsfront_end/models/ai_assistance/AiConversation.tsfront_end/models/ai_assistance/agents/ContextSelectionAgent.test.tsfront_end/models/ai_assistance/AiConversation.test.ts
Patch
From ec847bd3bc84f046dfd80e0f25cacc322cf09689 Mon Sep 17 00:00:00 2001
From: Danil Somsikov <dsv@chromium.org>
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 <alinavarkki@chromium.org>
Commit-Queue: Danil Somsikov <dsv@chromium.org>
---
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.
References
On This Page