From 1befabe58656932f50e146486fedcaed5487338e Mon Sep 17 00:00:00 2001 From: JerryLee <223425819+Jerry2003826@users.noreply.github.com> Date: Tue, 28 Apr 2026 01:31:37 +1000 Subject: [PATCH] fix(core): handle shell line continuations in command splitting (#3600) Fixes #3158. `splitCommands()` previously handled backslash escapes before newline/operator splitting, so a chained command like `cd project && \\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 \\rm -rf /`. Adds regression coverage for both the LF-continuation positive case and the escaped-CRLF safety case. --- packages/core/src/utils/shell-utils.test.ts | 12 ++++++++++++ packages/core/src/utils/shell-utils.ts | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/packages/core/src/utils/shell-utils.test.ts b/packages/core/src/utils/shell-utils.test.ts index b95e95c7b..f03814268 100644 --- a/packages/core/src/utils/shell-utils.test.ts +++ b/packages/core/src/utils/shell-utils.test.ts @@ -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']); diff --git a/packages/core/src/utils/shell-utils.ts b/packages/core/src/utils/shell-utils.ts index ac4a2d0d5..55d778dde 100644 --- a/packages/core/src/utils/shell-utils.ts +++ b/packages/core/src/utils/shell-utils.ts @@ -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;