fix(core): handle shell line continuations in command splitting (#3600)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run

Fixes #3158.

`splitCommands()` previously handled backslash escapes before newline/operator splitting, so a chained command like `cd project && \\<LF>git add ...` produced segments starting with a backslash-newline pair, leaving the extracted command root empty and bypassing per-command permission checks for the chained sub-command.

Treat `\\` followed by LF as a removed line continuation, while keeping `\\` followed by CRLF as a normal command separator (bash escapes only \r; the trailing \n still ends the command). This preserves the contract that every chained sub-command is visible to permission parsing and prevents an attacker from hiding a command behind a pseudo-continuation like `echo SAFE \\<CR><LF>rm -rf /`.

Adds regression coverage for both the LF-continuation positive case and the escaped-CRLF safety case.
This commit is contained in:
JerryLee 2026-04-28 01:31:37 +10:00 committed by GitHub
parent 414b3304cd
commit 1befabe586
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 17 additions and 0 deletions

View file

@ -442,6 +442,18 @@ describe('getCommandRoots', () => {
expect(result).toEqual(['grep']);
});
it('should treat escaped newlines in chained commands as line continuations', async () => {
const result = getCommandRoots(
'cd project && \\\ngit add file.php && \\\ngit commit -m "feat"',
);
expect(result).toEqual(['cd', 'git', 'git']);
});
it('should not treat escaped CRLF as a line continuation', async () => {
const result = getCommandRoots('echo SAFE \\\r\nrm -rf /');
expect(result).toEqual(['echo', 'rm']);
});
it('should filter out empty segments from consecutive newlines', async () => {
const result = getCommandRoots('ls\n\ngrep foo');
expect(result).toEqual(['ls', 'grep']);

View file

@ -224,6 +224,11 @@ export function splitCommands(command: string): string[] {
const char = command[i];
const nextChar = command[i + 1];
if (!inSingleQuotes && char === '\\' && nextChar === '\n') {
i += 2;
continue;
}
if (char === '\\' && i < command.length - 1) {
currentCommand += char + command[i + 1];
i += 2;