fix(exec): allow heredoc operator (<<) in allowlist security mode (#13811)

* fix(exec): allow heredoc operator (<<) in allowlist security mode

* fix: allow multiline heredoc parsing in exec approvals (#13811) (thanks @mcaxtr)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Marcus Castro
2026-02-13 00:41:51 -03:00
committed by GitHub
parent 7c25696ab0
commit e90caa66d8
3 changed files with 209 additions and 55 deletions

View File

@@ -164,6 +164,68 @@ describe("exec approvals shell parsing", () => {
expect(res.segments[0]?.argv[0]).toBe("echo");
});
it("rejects input redirection (<)", () => {
const res = analyzeShellCommand({ command: "cat < input.txt" });
expect(res.ok).toBe(false);
expect(res.reason).toBe("unsupported shell token: <");
});
it("rejects output redirection (>)", () => {
const res = analyzeShellCommand({ command: "echo ok > output.txt" });
expect(res.ok).toBe(false);
expect(res.reason).toBe("unsupported shell token: >");
});
it("allows heredoc operator (<<)", () => {
const res = analyzeShellCommand({ command: "/usr/bin/tee /tmp/file << 'EOF'" });
expect(res.ok).toBe(true);
expect(res.segments[0]?.argv[0]).toBe("/usr/bin/tee");
});
it("allows heredoc without space before delimiter", () => {
const res = analyzeShellCommand({ command: "/usr/bin/tee /tmp/file <<EOF" });
expect(res.ok).toBe(true);
expect(res.segments[0]?.argv[0]).toBe("/usr/bin/tee");
});
it("allows heredoc with strip-tabs operator (<<-)", () => {
const res = analyzeShellCommand({ command: "/usr/bin/cat <<-DELIM" });
expect(res.ok).toBe(true);
expect(res.segments[0]?.argv[0]).toBe("/usr/bin/cat");
});
it("allows heredoc in pipeline", () => {
const res = analyzeShellCommand({ command: "/usr/bin/cat << 'EOF' | /usr/bin/grep pattern" });
expect(res.ok).toBe(true);
expect(res.segments).toHaveLength(2);
expect(res.segments[0]?.argv[0]).toBe("/usr/bin/cat");
expect(res.segments[1]?.argv[0]).toBe("/usr/bin/grep");
});
it("allows multiline heredoc body", () => {
const res = analyzeShellCommand({
command: "/usr/bin/tee /tmp/file << 'EOF'\nline one\nline two\nEOF",
});
expect(res.ok).toBe(true);
expect(res.segments[0]?.argv[0]).toBe("/usr/bin/tee");
});
it("allows multiline heredoc body with strip-tabs operator (<<-)", () => {
const res = analyzeShellCommand({
command: "/usr/bin/cat <<-EOF\n\tline one\n\tline two\n\tEOF",
});
expect(res.ok).toBe(true);
expect(res.segments[0]?.argv[0]).toBe("/usr/bin/cat");
});
it("rejects multiline commands without heredoc", () => {
const res = analyzeShellCommand({
command: "/usr/bin/echo first line\n/usr/bin/echo second line",
});
expect(res.ok).toBe(false);
expect(res.reason).toBe("unsupported shell token: \n");
});
it("rejects windows shell metacharacters", () => {
const res = analyzeShellCommand({
command: "ping 127.0.0.1 -n 1 & whoami",