Chrome · DevTools
CVE-2026-14116
Logic Error in DevTools
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
forfront_end/models/ai_assistance/agents/AiAgent.ts |
modified | |
iffront_end/models/ai_assistance/agents/AiAgent.ts |
modified |
Files Changed
front_end/models/ai_assistance/agents/AiAgent.test.tsfront_end/models/ai_assistance/agents/AiAgent.tsfront_end/panels/ai_assistance/components/ChatInput.test.tsfront_end/panels/ai_assistance/components/ChatInput.ts
Patch
From 881a52b63673d62e682ace4e5d91084c534bf5e3 Mon Sep 17 00:00:00 2001
From: Kim-Anh Tran <kimanh@chromium.org>
Date: Wed, 20 May 2026 15:05:30 +0200
Subject: [PATCH] Sanitize suggestion chips for Ai Assistance
This truncates and adds basic sanitization of the suggestion chips.
We check if it is indeed an array of strings, and remove all
unnecessary extra whitespace.
We set the max length of a chip to be 200 characters, as chips
are suggestions that shouldn't be too long anyway. And we now also
respect the max length of the input text area to avoid users not
seeing what they insert via clicking on a chip.
Bug: 513747800
Change-Id: Ib0edcf30981d8446d0a691853308160740d0436c
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7864200
Reviewed-by: Alina Varkki <alinavarkki@chromium.org>
Commit-Queue: Kim-Anh Tran <kimanh@chromium.org>
---
diff --git a/front_end/models/ai_assistance/agents/AiAgent.test.ts b/front_end/models/ai_assistance/agents/AiAgent.test.ts
index 7d56060..25b082e 100644
--- a/front_end/models/ai_assistance/agents/AiAgent.test.ts
+++ b/front_end/models/ai_assistance/agents/AiAgent.test.ts
@@ -551,4 +551,88 @@
});
});
});
+
+ describe('parseTextResponseForSuggestions', () => {
+ it('should parse valid suggestions', () => {
+ const agent = new AiAgentMock({
+ aidaClient: mockAidaClient(),
+ });
+ const parsed = agent.parseTextResponseForSuggestions('SUGGESTIONS: ["how to fix", "why it fails"]');
+ assert.deepEqual(parsed.suggestions, ['how to fix', 'why it fails']);
+ });
+
+ it('should filter out non-string suggestions', () => {
+ const agent = new AiAgentMock({
+ aidaClient: mockAidaClient(),
+ });
+ const parsed = agent.parseTextResponseForSuggestions('SUGGESTIONS: ["valid", 123, null, {"key": "val"}]');
+ assert.deepEqual(parsed.suggestions, ['valid']);
+ });
+
+ it('should truncate long suggestions', () => {
+ const agent = new AiAgentMock({
+ aidaClient: mockAidaClient(),
+ });
+ const longSuggestion = 'a'.repeat(300);
+ const parsed = agent.parseTextResponseForSuggestions(`SUGGESTIONS: ["${longSuggestion}"]`);
+ assert.isDefined(parsed.suggestions);
+ assert.lengthOf(parsed.suggestions![0], 200);
+ assert.strictEqual(parsed.suggestions![0], 'a'.repeat(200));
+ });
+
+ it('should sanitize whitespace and newlines in suggestions', () => {
+ const agent = new AiAgentMock({
+ aidaClient: mockAidaClient(),
+ });
+ const parsed = agent.parseTextResponseForSuggestions(
+ 'SUGGESTIONS: ["line1\\nline2", "word1\\r\\nword2", "excessive spaces"]');
+ assert.deepEqual(parsed.suggestions, ['line1 line2', 'word1 word2', 'excessive spaces']);
+ });
+
+ it('should reject non-array suggestions', () => {
+ const agent = new AiAgentMock({
+ aidaClient: mockAidaClient(),
+ });
+ const parsed = agent.parseTextResponseForSuggestions('SUGGESTIONS: "not an array"');
+ assert.isUndefined(parsed.suggestions);
+ });
+
+ it('should remove empty suggestions after sanitization', () => {
+ const agent = new AiAgentMock({
+ aidaClient: mockAidaClient(),
+ });
+ const parsed = agent.parseTextResponseForSuggestions('SUGGESTIONS: ["", " ", "\\n\\n"]');
+ assert.isUndefined(parsed.suggestions);
+ });
+
+ it('should parse suggestions from a multi-line response containing both answer and suggestions', () => {
+ const agent = new AiAgentMock({
+ aidaClient: mockAidaClient(),
+ });
+ const responseText = [
+ 'Here is the first line of the answer.',
+ 'SUGGESTIONS: ["suggestion 1", "suggestion 2"]',
+ 'Here is the second line of the answer.',
+ ].join('\n');
+ const parsed = agent.parseTextResponseForSuggestions(responseText);
+ assert.strictEqual(
+ parsed.answer, 'Here is the first line of the answer.\nHere is the second line of the answer.');
+ assert.deepEqual(parsed.suggestions, ['suggestion 1', 'suggestion 2']);
+ });
+
+ it('should handle multiple SUGGESTIONS lines by keeping the last valid one', () => {
+ const agent = new AiAgentMock({
+ aidaClient: mockAidaClient(),
+ });
+ const responseText = [
+ 'Answer text.',
+ 'SUGGESTIONS: ["first suggestion"]',
+ 'More answer text.',
+ 'SUGGESTIONS: ["second suggestion"]',
+ ].join('\n');
+ const parsed = agent.parseTextResponseForSuggestions(responseText);
+ assert.strictEqual(parsed.answer, 'Answer text.\nMore answer text.');
+ assert.deepEqual(parsed.suggestions, ['second suggestion']);
+ });
+ });
});
diff --git a/front_end/models/ai_assistance/agents/AiAgent.ts b/front_end/models/ai_assistance/agents/AiAgent.ts
index a32362b..21927b5 100644
--- a/front_end/models/ai_assistance/agents/AiAgent.ts
+++ b/front_end/models/ai_assistance/agents/AiAgent.ts
@@ -13,6 +13,8 @@
import type * as Workspace from '../../workspace/workspace.js';
import {debugLog, isStructuredLogEnabled} from '../debug.js';
+const MAX_SUGGESTION_LENGTH = 200;
+
export const enum ResponseType {
CONTEXT = 'context',
TITLE = 'title',
@@ -575,8 +577,7 @@
const trimmed = line.trim();
if (trimmed.startsWith('SUGGESTIONS:')) {
try {
- // TODO: Do basic validation this is an array with strings
- suggestions = JSON.parse(trimmed.substring('SUGGESTIONS:'.length).trim());
+ suggestions = sanitizeSuggestions(trimmed.substring('SUGGESTIONS:'.length).trim());
} catch {
}
} else {
@@ -589,8 +590,7 @@
if (!suggestions && answerLines.at(-1)?.includes('SUGGESTIONS:')) {
const [answer, suggestionsText] = answerLines[answerLines.length - 1].split('SUGGESTIONS:', 2);
try {
- // TODO: Do basic validation this is an array with strings
- suggestions = JSON.parse(suggestionsText.trim().substring('SUGGESTIONS:'.length).trim());
+ suggestions = sanitizeSuggestions(suggestionsText.trim());
} catch {
}
answerLines[answerLines.length - 1] = answer;
@@ -997,3 +997,26 @@
};
}
}
+
+function sanitizeSuggestions(suggestions: string): [string, ...string[]]|undefined {
+ const parsed = JSON.parse(suggestions);
+ if (!Array.isArray(parsed)) {
+ return undefined;
+ }
+ const sanitized: string[] = [];
+ for (const item of parsed) {
+ if (typeof item !== 'string') {
+ continue;
+ }
+ // Collapse multiple whitespace/newlines into a single space.
+ const noExtraWhitespace = item.replace(/\s+/g, ' ').trim();
+ if (noExtraWhitespace.length === 0) {
+ continue;
+ }
+ sanitized.push(noExtraWhitespace.substring(0, MAX_SUGGESTION_LENGTH));
+ }
+ if (sanitized.length === 0) {
+ return undefined;
+ }
+ return sanitized as [string, ...string[]];
+}
diff --git a/front_end/panels/ai_assistance/components/ChatInput.test.ts b/front_end/panels/ai_assistance/components/ChatInput.test.ts
index 1b68329..997865d 100644
--- a/front_end/panels/ai_assistance/components/ChatInput.test.ts
+++ b/front_end/panels/ai_assistance/components/ChatInput.test.ts
@@ -69,6 +69,17 @@
assert.isTrue(view.input.isReadOnly);
});
+ it('should truncate input value to maxlength in setInputValue', async () => {
+ const [view, component] = createComponent();
+ const mockTextArea = document.createElement('textarea');
+ mockTextArea.maxLength = 10;
+ assert.isDefined(view.input.textAreaRef);
+ (view.input.textAreaRef as {value: HTMLTextAreaElement}).value = mockTextArea;
+
+ component.setInputValue('a'.repeat(20));
+ assert.strictEqual(mockTextArea.value, 'a'.repeat(10));
+ });
+
describe('multimodal input', () => {
let target: SDK.Target.Target;
let model: SDK.ScreenCaptureModel.ScreenCaptureModel;
diff --git a/front_end/panels/ai_assistance/components/ChatInput.ts b/front_end/panels/ai_assistance/components/ChatInput.ts
index 8c67fe1..44894d5 100644
--- a/front_end/panels/ai_assistance/components/ChatInput.ts
+++ b/front_end/panels/ai_assistance/components/ChatInput.ts
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