mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-10 20:30:13 +00:00
fix(cli): sanitize ANSI escape sequences in /tasks output
deepseek's review pass flagged that monitor description / error fields are user / process-supplied strings rendered directly to the terminal. A maliciously-crafted tool description or spawn error containing raw ANSI control sequences (clear-screen, cursor-move, colour) would otherwise reach stdout verbatim and corrupt display. Same risk applies to agent error / description and shell error / command — all already-existing renderers with the same exposure that this PR didn't introduce but inherits. So instead of per-field sprinkling, wrap the joined output once with `escapeAnsiCtrlCodes` (no-op when no control chars present, so cost is zero in the common case). One line change in the renderer covers every kind including any future one. Test pins the behaviour: a monitor entry with `\x1b[2J` / `\x1b[31m...` content produces output with no raw ESC bytes and visible escaped `[...]` sequences.
This commit is contained in:
parent
040dab2ada
commit
0840e32f6b
2 changed files with 36 additions and 1 deletions
|
|
@ -324,6 +324,32 @@ describe('tasksCommand', () => {
|
|||
expect(result.content).toContain('Background tasks (1 total)');
|
||||
});
|
||||
|
||||
it('escapes ANSI control sequences embedded in entry fields', async () => {
|
||||
// A monitor whose description / error contain raw ANSI escape codes
|
||||
// (e.g. `\x1b[2J` clear-screen, `\x1b[31m...` colour) must not reach
|
||||
// the terminal verbatim — a malicious tool description or spawn
|
||||
// error otherwise could corrupt display. `escapeAnsiCtrlCodes`
|
||||
// converts them to literal `[...]` sequences that render as
|
||||
// text instead of being interpreted by the terminal.
|
||||
getMonitors.mockReturnValue([
|
||||
monitorEntry({
|
||||
monitorId: 'mon_evil',
|
||||
description: 'bad \x1b[2Jthing',
|
||||
status: 'failed',
|
||||
error: 'spawn \x1b[31mENOENT\x1b[0m',
|
||||
eventCount: 0,
|
||||
}),
|
||||
]);
|
||||
const result = await tasksCommand.action!(context, '');
|
||||
if (!result || result.type !== 'message') throw new Error('no result');
|
||||
// No raw ESC byte in the output.
|
||||
expect(result.content).not.toContain('\x1b');
|
||||
// The escaped form (literal backslash-u-001b) should appear, proving
|
||||
// the sanitizer ran rather than dropping the chars.
|
||||
expect(result.content).toContain('\\u001b[2J');
|
||||
expect(result.content).toContain('\\u001b[31m');
|
||||
});
|
||||
|
||||
it('merges all three kinds and orders them by startTime', async () => {
|
||||
const now = Date.now();
|
||||
getAgents.mockReturnValue([
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import type { SlashCommand } from './types.js';
|
|||
import { CommandKind } from './types.js';
|
||||
import { t } from '../../i18n/index.js';
|
||||
import { formatDuration } from '../utils/formatters.js';
|
||||
import { escapeAnsiCtrlCodes } from '../utils/textUtils.js';
|
||||
|
||||
type AgentTaskEntry = BackgroundTaskEntry & {
|
||||
kind: 'agent';
|
||||
|
|
@ -225,10 +226,18 @@ export const tasksCommand: SlashCommand = {
|
|||
}
|
||||
}
|
||||
|
||||
// Defense in depth: registry entries carry user-supplied / process-
|
||||
// supplied strings (description, command, error from spawn / settle).
|
||||
// A maliciously-crafted value containing raw ANSI escape sequences
|
||||
// could otherwise reach the terminal verbatim and corrupt display.
|
||||
// `escapeAnsiCtrlCodes` is a no-op when the string has no control
|
||||
// chars, so wrapping the joined output is cheap and covers every
|
||||
// field — including any future kind's fields — without per-site
|
||||
// sanitization sprawl.
|
||||
return {
|
||||
type: 'message' as const,
|
||||
messageType: 'info' as const,
|
||||
content: lines.join('\n'),
|
||||
content: escapeAnsiCtrlCodes(lines.join('\n')),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue