Skip to content

Commit 9a40091

Browse files
committed
fix: prevent terminal commands from bypassing .rooignore restrictions
- Enhanced RooIgnoreController.validateCommand() to detect shell redirections, command substitutions, and process substitutions - Added detection for additional file-reading commands (nl, tac, strings, hexdump, od, zcat, diff, etc.) - Added comprehensive test coverage for new command validation patterns - Fixes #7204
1 parent 87c42c1 commit 9a40091

File tree

2 files changed

+220
-34
lines changed

2 files changed

+220
-34
lines changed

src/core/ignore/RooIgnoreController.ts

Lines changed: 124 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -114,44 +114,134 @@ export class RooIgnoreController {
114114
return undefined
115115
}
116116

117-
// Split command into parts and get the base command
118-
const parts = command.trim().split(/\s+/)
119-
const baseCommand = parts[0].toLowerCase()
120-
121-
// Commands that read file contents
122-
const fileReadingCommands = [
123-
// Unix commands
124-
"cat",
125-
"less",
126-
"more",
127-
"head",
128-
"tail",
129-
"grep",
130-
"awk",
131-
"sed",
132-
// PowerShell commands and aliases
133-
"get-content",
134-
"gc",
135-
"type",
136-
"select-string",
137-
"sls",
117+
// First, check for shell redirections and command substitutions that could read files
118+
// These patterns can bypass simple command parsing
119+
const dangerousPatterns = [
120+
// Input redirection: < file, <file
121+
/<\s*([^\s<>|;&]+)/g,
122+
// Command substitution: $(cat file), `cat file`
123+
/\$\([^)]*\b(cat|head|tail|less|more|grep|awk|sed|type|gc|get-content)\s+([^\s)]+)[^)]*\)/gi,
124+
/`[^`]*\b(cat|head|tail|less|more|grep|awk|sed|type|gc|get-content)\s+([^\s`]+)[^`]*`/gi,
125+
// Process substitution: <(cat file)
126+
/<\([^)]*\b(cat|head|tail|less|more|grep|awk|sed|type|gc|get-content)\s+([^\s)]+)[^)]*\)/gi,
127+
// Here documents/strings that might reference files
128+
/<<<?\s*([^\s<>|;&]+)/g,
138129
]
139130

140-
if (fileReadingCommands.includes(baseCommand)) {
141-
// Check each argument that could be a file path
142-
for (let i = 1; i < parts.length; i++) {
143-
const arg = parts[i]
144-
// Skip command flags/options (both Unix and PowerShell style)
145-
if (arg.startsWith("-") || arg.startsWith("/")) {
146-
continue
131+
for (const pattern of dangerousPatterns) {
132+
const matches = command.matchAll(pattern)
133+
for (const match of matches) {
134+
// Get the potential file path from the match
135+
// Different patterns have the file path at different indices
136+
const potentialPaths = [match[1], match[2], match[3]].filter(Boolean)
137+
for (const filePath of potentialPaths) {
138+
if (filePath && !this.validateAccess(filePath)) {
139+
return filePath
140+
}
147141
}
148-
// Ignore PowerShell parameter names
149-
if (arg.includes(":")) {
150-
continue
142+
}
143+
}
144+
145+
// Check for piped commands that might expose file contents
146+
// e.g., echo "$(cat file)" or echo `cat file`
147+
const pipelineCommands = command.split(/[|;&]/).map((cmd) => cmd.trim())
148+
149+
for (const pipeCmd of pipelineCommands) {
150+
// Split command into parts and get the base command
151+
const parts = pipeCmd.split(/\s+/)
152+
if (parts.length === 0) continue
153+
154+
const baseCommand = parts[0].toLowerCase()
155+
156+
// Commands that read file contents
157+
const fileReadingCommands = [
158+
// Unix commands
159+
"cat",
160+
"less",
161+
"more",
162+
"head",
163+
"tail",
164+
"grep",
165+
"awk",
166+
"sed",
167+
"nl",
168+
"tac",
169+
"rev",
170+
"cut",
171+
"paste",
172+
"sort",
173+
"uniq",
174+
"comm",
175+
"diff",
176+
"cmp",
177+
"od",
178+
"hexdump",
179+
"xxd",
180+
"strings",
181+
"file",
182+
// Additional Unix utilities
183+
"zcat",
184+
"zless",
185+
"zmore",
186+
"bzcat",
187+
"xzcat",
188+
"view",
189+
// PowerShell commands and aliases
190+
"get-content",
191+
"gc",
192+
"type",
193+
"select-string",
194+
"sls",
195+
// Windows commands
196+
"findstr",
197+
"find",
198+
"fc",
199+
]
200+
201+
if (fileReadingCommands.includes(baseCommand)) {
202+
// Check each argument that could be a file path
203+
for (let i = 1; i < parts.length; i++) {
204+
const arg = parts[i]
205+
// Skip command flags/options (both Unix and PowerShell style)
206+
if (arg.startsWith("-") || arg.startsWith("/")) {
207+
continue
208+
}
209+
// Ignore PowerShell parameter names
210+
if (arg.includes(":") && i > 0 && parts[i - 1].startsWith("-")) {
211+
continue
212+
}
213+
// Skip empty arguments
214+
if (!arg) {
215+
continue
216+
}
217+
// Remove quotes if present
218+
const cleanArg = arg.replace(/^["']|["']$/g, "")
219+
// Validate file access
220+
if (!this.validateAccess(cleanArg)) {
221+
return cleanArg
222+
}
151223
}
152-
// Validate file access
153-
if (!this.validateAccess(arg)) {
154-
return arg
224+
}
225+
226+
// Also check for commands that might read files indirectly
227+
// e.g., xargs cat, find -exec cat, etc.
228+
if (baseCommand === "xargs" || baseCommand === "find") {
229+
// Look for file-reading commands in the arguments
230+
const argsStr = parts.slice(1).join(" ")
231+
for (const readCmd of fileReadingCommands) {
232+
if (argsStr.includes(readCmd)) {
233+
// Try to extract file paths from find patterns or xargs input
234+
// This is complex, so we'll check common patterns
235+
const filePatterns = argsStr.match(/(?:name|path)\s+["']?([^"'\s]+)["']?/gi)
236+
if (filePatterns) {
237+
for (const pattern of filePatterns) {
238+
const filePath = pattern.replace(/(?:name|path)\s+["']?([^"'\s]+)["']?/i, "$1")
239+
if (!this.validateAccess(filePath)) {
240+
return filePath
241+
}
242+
}
243+
}
244+
}
155245
}
156246
}
157247
}

src/core/ignore/__tests__/RooIgnoreController.spec.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,100 @@ describe("RooIgnoreController", () => {
275275
expect(controller.validateCommand("npm install")).toBeUndefined()
276276
})
277277

278+
/**
279+
* Tests validation of shell redirections and command substitutions
280+
*/
281+
it("should block shell redirections that read ignored files", () => {
282+
// Input redirection
283+
expect(controller.validateCommand("wc -l < node_modules/package.json")).toBe("node_modules/package.json")
284+
expect(controller.validateCommand("sort < secrets/api-keys.json")).toBe("secrets/api-keys.json")
285+
expect(controller.validateCommand("grep pattern <.git/config")).toBe(".git/config")
286+
287+
// Should allow non-ignored files
288+
expect(controller.validateCommand("wc -l < README.md")).toBeUndefined()
289+
})
290+
291+
it("should block command substitutions that read ignored files", () => {
292+
// $() command substitution
293+
expect(controller.validateCommand("echo $(cat node_modules/package.json)")).toBe(
294+
"node_modules/package.json",
295+
)
296+
expect(controller.validateCommand("result=$(head secrets/api-keys.json)")).toBe("secrets/api-keys.json")
297+
298+
// Backtick command substitution
299+
expect(controller.validateCommand("echo `cat .git/config`")).toBe(".git/config")
300+
expect(controller.validateCommand("data=`tail error.log`")).toBe("error.log")
301+
302+
// Process substitution
303+
expect(controller.validateCommand("diff <(cat node_modules/index.js) file2")).toBe("node_modules/index.js")
304+
305+
// Should allow non-ignored files
306+
expect(controller.validateCommand("echo $(cat README.md)")).toBeUndefined()
307+
})
308+
309+
it("should block piped commands that read ignored files", () => {
310+
// Commands in pipelines
311+
expect(controller.validateCommand("cat node_modules/package.json | grep version")).toBe(
312+
"node_modules/package.json",
313+
)
314+
expect(controller.validateCommand("echo test | tee secrets/output.log; cat secrets/output.log")).toBe(
315+
"secrets/output.log",
316+
)
317+
expect(controller.validateCommand("ls && head .git/config")).toBe(".git/config")
318+
319+
// Should allow non-ignored files in pipelines
320+
expect(controller.validateCommand("cat README.md | grep title")).toBeUndefined()
321+
})
322+
323+
it("should detect additional file reading commands", () => {
324+
// Additional Unix utilities
325+
expect(controller.validateCommand("nl node_modules/package.json")).toBe("node_modules/package.json")
326+
expect(controller.validateCommand("tac .git/config")).toBe(".git/config")
327+
expect(controller.validateCommand("strings secrets/binary.dat")).toBe("secrets/binary.dat")
328+
expect(controller.validateCommand("hexdump error.log")).toBe("error.log")
329+
expect(controller.validateCommand("od -c node_modules/index.js")).toBe("node_modules/index.js")
330+
331+
// Compressed file readers
332+
expect(controller.validateCommand("zcat secrets/data.gz")).toBe("secrets/data.gz")
333+
expect(controller.validateCommand("bzcat node_modules/archive.bz2")).toBe("node_modules/archive.bz2")
334+
335+
// File comparison utilities
336+
expect(controller.validateCommand("diff .git/config file2")).toBe(".git/config")
337+
expect(controller.validateCommand("cmp secrets/key1 secrets/key2")).toBe("secrets/key1")
338+
339+
// Windows/PowerShell commands
340+
expect(controller.validateCommand("findstr pattern node_modules/package.json")).toBe(
341+
"node_modules/package.json",
342+
)
343+
expect(controller.validateCommand("fc .git/config file2")).toBe(".git/config")
344+
})
345+
346+
it("should handle complex command patterns", () => {
347+
// Commands with quotes
348+
expect(controller.validateCommand('cat "node_modules/package.json"')).toBe("node_modules/package.json")
349+
expect(controller.validateCommand("cat 'secrets/api-keys.json'")).toBe("secrets/api-keys.json")
350+
351+
// Multiple files in one command
352+
expect(controller.validateCommand("cat README.md node_modules/index.js")).toBe("node_modules/index.js")
353+
354+
// Nested command substitutions
355+
expect(controller.validateCommand("echo $(echo $(cat .git/config))")).toBe(".git/config")
356+
357+
// Mixed patterns
358+
expect(controller.validateCommand("cat < node_modules/data.txt | grep pattern")).toBe(
359+
"node_modules/data.txt",
360+
)
361+
})
362+
363+
it("should handle xargs and find commands that might read files", () => {
364+
// xargs with file reading commands
365+
expect(controller.validateCommand("find . -name '*.json' | xargs cat")).toBeUndefined() // Can't determine specific files
366+
expect(controller.validateCommand("echo node_modules/package.json | xargs cat")).toBeUndefined() // File path is in stdin, not command
367+
368+
// find with -exec
369+
expect(controller.validateCommand("find . -name 'package.json' -exec cat {} \\;")).toBeUndefined() // Can't determine specific files
370+
})
371+
278372
/**
279373
* Tests behavior when no .rooignore exists
280374
*/
@@ -287,6 +381,8 @@ describe("RooIgnoreController", () => {
287381
// All commands should be allowed
288382
expect(emptyController.validateCommand("cat node_modules/package.json")).toBeUndefined()
289383
expect(emptyController.validateCommand("grep pattern .git/config")).toBeUndefined()
384+
expect(emptyController.validateCommand("echo $(cat secrets/file)")).toBeUndefined()
385+
expect(emptyController.validateCommand("wc < .git/config")).toBeUndefined()
290386
})
291387
})
292388

0 commit comments

Comments
 (0)