Skip to content

Commit 81082b9

Browse files
authored
Merge pull request #1601 from QwenLM/fix/awk-system-command-security
Security: Fix awk/sed Command Injection in READ_ONLY_ROOT_COMMANDS
2 parents 829ba9c + e615438 commit 81082b9

File tree

2 files changed

+155
-0
lines changed

2 files changed

+155
-0
lines changed

packages/core/src/utils/shellReadOnlyChecker.test.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,106 @@ describe('evaluateShellCommandReadOnly', () => {
5353
const result = isShellCommandReadOnly('FOO=bar ls');
5454
expect(result).toBe(true);
5555
});
56+
57+
describe('awk command security', () => {
58+
it('allows safe awk commands', () => {
59+
expect(isShellCommandReadOnly("awk '{print $1}' file.txt")).toBe(true);
60+
expect(isShellCommandReadOnly('awk \'BEGIN {print "hello"}\'')).toBe(
61+
true,
62+
);
63+
expect(isShellCommandReadOnly("awk '/pattern/ {print}' file.txt")).toBe(
64+
true,
65+
);
66+
});
67+
68+
it('rejects awk with system() calls', () => {
69+
expect(isShellCommandReadOnly('awk \'BEGIN {system("rm -rf /")}\'')).toBe(
70+
false,
71+
);
72+
expect(
73+
isShellCommandReadOnly('awk \'{system("touch file")}\' input.txt'),
74+
).toBe(false);
75+
expect(isShellCommandReadOnly('awk \'BEGIN { system ( "ls" ) }\'')).toBe(
76+
false,
77+
);
78+
});
79+
80+
it('rejects awk with file output redirection', () => {
81+
expect(
82+
isShellCommandReadOnly('awk \'{print > "output.txt"}\' input.txt'),
83+
).toBe(false);
84+
expect(
85+
isShellCommandReadOnly('awk \'{printf "%s\\n", $0 > "file.txt"}\''),
86+
).toBe(false);
87+
expect(
88+
isShellCommandReadOnly('awk \'{print >> "append.txt"}\' input.txt'),
89+
).toBe(false);
90+
expect(
91+
isShellCommandReadOnly('awk \'{printf "%s" >> "file.txt"}\''),
92+
).toBe(false);
93+
});
94+
95+
it('rejects awk with command pipes', () => {
96+
expect(isShellCommandReadOnly('awk \'{print | "sort"}\' input.txt')).toBe(
97+
false,
98+
);
99+
expect(
100+
isShellCommandReadOnly('awk \'{printf "%s\\n", $0 | "wc -l"}\''),
101+
).toBe(false);
102+
});
103+
104+
it('rejects awk with getline from commands', () => {
105+
expect(isShellCommandReadOnly('awk \'BEGIN {getline < "date"}\'')).toBe(
106+
false,
107+
);
108+
expect(isShellCommandReadOnly('awk \'BEGIN {"date" | getline}\'')).toBe(
109+
false,
110+
);
111+
});
112+
113+
it('rejects awk with close() calls', () => {
114+
expect(isShellCommandReadOnly('awk \'BEGIN {close("file")}\'')).toBe(
115+
false,
116+
);
117+
expect(isShellCommandReadOnly("awk '{close(cmd)}' input.txt")).toBe(
118+
false,
119+
);
120+
});
121+
});
122+
123+
describe('sed command security', () => {
124+
it('allows safe sed commands', () => {
125+
expect(isShellCommandReadOnly("sed 's/foo/bar/' file.txt")).toBe(true);
126+
expect(isShellCommandReadOnly("sed -n '1,5p' file.txt")).toBe(true);
127+
expect(isShellCommandReadOnly("sed '/pattern/d' file.txt")).toBe(true);
128+
});
129+
130+
it('rejects sed with execute command', () => {
131+
expect(isShellCommandReadOnly("sed 's/foo/bar/e' file.txt")).toBe(false);
132+
expect(isShellCommandReadOnly("sed 'e date' file.txt")).toBe(false);
133+
});
134+
135+
it('rejects sed with write command', () => {
136+
expect(
137+
isShellCommandReadOnly("sed 's/foo/bar/w output.txt' file.txt"),
138+
).toBe(false);
139+
expect(isShellCommandReadOnly("sed 'w backup.txt' file.txt")).toBe(false);
140+
});
141+
142+
it('rejects sed with read command', () => {
143+
expect(
144+
isShellCommandReadOnly("sed 's/foo/bar/r input.txt' file.txt"),
145+
).toBe(false);
146+
expect(isShellCommandReadOnly("sed 'r header.txt' file.txt")).toBe(false);
147+
});
148+
149+
it('still rejects sed in-place editing', () => {
150+
expect(isShellCommandReadOnly("sed -i 's/foo/bar/' file.txt")).toBe(
151+
false,
152+
);
153+
expect(
154+
isShellCommandReadOnly("sed --in-place 's/foo/bar/' file.txt"),
155+
).toBe(false);
156+
});
157+
});
56158
});

packages/core/src/utils/shellReadOnlyChecker.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,30 @@ const BLOCKED_GIT_BRANCH_FLAGS = new Set([
9292

9393
const BLOCKED_SED_PREFIXES = ['-i'];
9494

95+
// AWK side-effect patterns that can execute commands or write files
96+
const AWK_SIDE_EFFECT_PATTERNS = [
97+
/system\s*\(/, // system() function calls
98+
/print\s+[^>|]*>\s*"[^"]*"/, // print > "file"
99+
/printf\s+[^>|]*>\s*"[^"]*"/, // printf > "file"
100+
/print\s+[^>|]*>>\s*"[^"]*"/, // print >> "file"
101+
/printf\s+[^>|]*>>\s*"[^"]*"/, // printf >> "file"
102+
/print\s+[^|]*\|\s*"[^"]*"/, // print | "command"
103+
/printf\s+[^|]*\|\s*"[^"]*"/, // printf | "command"
104+
/getline\s*<\s*"[^"]*"/, // getline < "command"
105+
/"[^"]*"\s*\|\s*getline/, // "command" | getline
106+
/close\s*\(/, // close() can trigger command execution
107+
];
108+
109+
// SED side-effect patterns
110+
const SED_SIDE_EFFECT_PATTERNS = [
111+
/[^\\]e\s/, // e command (execute)
112+
/^e\s/, // e command at start
113+
/[^\\]w\s/, // w command (write)
114+
/^w\s/, // w command at start
115+
/[^\\]r\s/, // r command (read file)
116+
/^r\s/, // r command at start
117+
];
118+
95119
const ENV_ASSIGNMENT_REGEX = /^[A-Za-z_][A-Za-z0-9_]*=/;
96120

97121
function containsWriteRedirection(command: string): boolean {
@@ -182,6 +206,31 @@ function evaluateSedCommand(tokens: string[]): boolean {
182206
return false;
183207
}
184208
}
209+
210+
// Check for side-effect patterns in sed script
211+
const scriptContent = rest.join(' ');
212+
for (const pattern of SED_SIDE_EFFECT_PATTERNS) {
213+
if (pattern.test(scriptContent)) {
214+
return false;
215+
}
216+
}
217+
218+
return true;
219+
}
220+
221+
function evaluateAwkCommand(tokens: string[]): boolean {
222+
const [, ...rest] = tokens;
223+
224+
// Join all arguments to check for awk script content
225+
const scriptContent = rest.join(' ');
226+
227+
// Check for dangerous side-effect patterns
228+
for (const pattern of AWK_SIDE_EFFECT_PATTERNS) {
229+
if (pattern.test(scriptContent)) {
230+
return false;
231+
}
232+
}
233+
185234
return true;
186235
}
187236

@@ -276,6 +325,10 @@ function evaluateShellSegment(segment: string): boolean {
276325
return evaluateSedCommand([normalizedRoot, ...args]);
277326
}
278327

328+
if (normalizedRoot === 'awk') {
329+
return evaluateAwkCommand([normalizedRoot, ...args]);
330+
}
331+
279332
if (normalizedRoot === 'git') {
280333
return evaluateGitCommand([normalizedRoot, ...args]);
281334
}

0 commit comments

Comments
 (0)