mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-09 22:31:08 +00:00
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
This commit is contained in:
parent
87c42c1f26
commit
9a400917a6
2 changed files with 220 additions and 34 deletions
|
|
@ -114,44 +114,134 @@ export class RooIgnoreController {
|
|||
return undefined
|
||||
}
|
||||
|
||||
// Split command into parts and get the base command
|
||||
const parts = command.trim().split(/\s+/)
|
||||
const baseCommand = parts[0].toLowerCase()
|
||||
|
||||
// Commands that read file contents
|
||||
const fileReadingCommands = [
|
||||
// Unix commands
|
||||
"cat",
|
||||
"less",
|
||||
"more",
|
||||
"head",
|
||||
"tail",
|
||||
"grep",
|
||||
"awk",
|
||||
"sed",
|
||||
// PowerShell commands and aliases
|
||||
"get-content",
|
||||
"gc",
|
||||
"type",
|
||||
"select-string",
|
||||
"sls",
|
||||
// First, check for shell redirections and command substitutions that could read files
|
||||
// These patterns can bypass simple command parsing
|
||||
const dangerousPatterns = [
|
||||
// Input redirection: < file, <file
|
||||
/<\s*([^\s<>|;&]+)/g,
|
||||
// Command substitution: $(cat file), `cat file`
|
||||
/\$\([^)]*\b(cat|head|tail|less|more|grep|awk|sed|type|gc|get-content)\s+([^\s)]+)[^)]*\)/gi,
|
||||
/`[^`]*\b(cat|head|tail|less|more|grep|awk|sed|type|gc|get-content)\s+([^\s`]+)[^`]*`/gi,
|
||||
// Process substitution: <(cat file)
|
||||
/<\([^)]*\b(cat|head|tail|less|more|grep|awk|sed|type|gc|get-content)\s+([^\s)]+)[^)]*\)/gi,
|
||||
// Here documents/strings that might reference files
|
||||
/<<<?\s*([^\s<>|;&]+)/g,
|
||||
]
|
||||
|
||||
if (fileReadingCommands.includes(baseCommand)) {
|
||||
// Check each argument that could be a file path
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const arg = parts[i]
|
||||
// Skip command flags/options (both Unix and PowerShell style)
|
||||
if (arg.startsWith("-") || arg.startsWith("/")) {
|
||||
continue
|
||||
for (const pattern of dangerousPatterns) {
|
||||
const matches = command.matchAll(pattern)
|
||||
for (const match of matches) {
|
||||
// Get the potential file path from the match
|
||||
// Different patterns have the file path at different indices
|
||||
const potentialPaths = [match[1], match[2], match[3]].filter(Boolean)
|
||||
for (const filePath of potentialPaths) {
|
||||
if (filePath && !this.validateAccess(filePath)) {
|
||||
return filePath
|
||||
}
|
||||
}
|
||||
// Ignore PowerShell parameter names
|
||||
if (arg.includes(":")) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Check for piped commands that might expose file contents
|
||||
// e.g., echo "$(cat file)" or echo `cat file`
|
||||
const pipelineCommands = command.split(/[|;&]/).map((cmd) => cmd.trim())
|
||||
|
||||
for (const pipeCmd of pipelineCommands) {
|
||||
// Split command into parts and get the base command
|
||||
const parts = pipeCmd.split(/\s+/)
|
||||
if (parts.length === 0) continue
|
||||
|
||||
const baseCommand = parts[0].toLowerCase()
|
||||
|
||||
// Commands that read file contents
|
||||
const fileReadingCommands = [
|
||||
// Unix commands
|
||||
"cat",
|
||||
"less",
|
||||
"more",
|
||||
"head",
|
||||
"tail",
|
||||
"grep",
|
||||
"awk",
|
||||
"sed",
|
||||
"nl",
|
||||
"tac",
|
||||
"rev",
|
||||
"cut",
|
||||
"paste",
|
||||
"sort",
|
||||
"uniq",
|
||||
"comm",
|
||||
"diff",
|
||||
"cmp",
|
||||
"od",
|
||||
"hexdump",
|
||||
"xxd",
|
||||
"strings",
|
||||
"file",
|
||||
// Additional Unix utilities
|
||||
"zcat",
|
||||
"zless",
|
||||
"zmore",
|
||||
"bzcat",
|
||||
"xzcat",
|
||||
"view",
|
||||
// PowerShell commands and aliases
|
||||
"get-content",
|
||||
"gc",
|
||||
"type",
|
||||
"select-string",
|
||||
"sls",
|
||||
// Windows commands
|
||||
"findstr",
|
||||
"find",
|
||||
"fc",
|
||||
]
|
||||
|
||||
if (fileReadingCommands.includes(baseCommand)) {
|
||||
// Check each argument that could be a file path
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const arg = parts[i]
|
||||
// Skip command flags/options (both Unix and PowerShell style)
|
||||
if (arg.startsWith("-") || arg.startsWith("/")) {
|
||||
continue
|
||||
}
|
||||
// Ignore PowerShell parameter names
|
||||
if (arg.includes(":") && i > 0 && parts[i - 1].startsWith("-")) {
|
||||
continue
|
||||
}
|
||||
// Skip empty arguments
|
||||
if (!arg) {
|
||||
continue
|
||||
}
|
||||
// Remove quotes if present
|
||||
const cleanArg = arg.replace(/^["']|["']$/g, "")
|
||||
// Validate file access
|
||||
if (!this.validateAccess(cleanArg)) {
|
||||
return cleanArg
|
||||
}
|
||||
}
|
||||
// Validate file access
|
||||
if (!this.validateAccess(arg)) {
|
||||
return arg
|
||||
}
|
||||
|
||||
// Also check for commands that might read files indirectly
|
||||
// e.g., xargs cat, find -exec cat, etc.
|
||||
if (baseCommand === "xargs" || baseCommand === "find") {
|
||||
// Look for file-reading commands in the arguments
|
||||
const argsStr = parts.slice(1).join(" ")
|
||||
for (const readCmd of fileReadingCommands) {
|
||||
if (argsStr.includes(readCmd)) {
|
||||
// Try to extract file paths from find patterns or xargs input
|
||||
// This is complex, so we'll check common patterns
|
||||
const filePatterns = argsStr.match(/(?:name|path)\s+["']?([^"'\s]+)["']?/gi)
|
||||
if (filePatterns) {
|
||||
for (const pattern of filePatterns) {
|
||||
const filePath = pattern.replace(/(?:name|path)\s+["']?([^"'\s]+)["']?/i, "$1")
|
||||
if (!this.validateAccess(filePath)) {
|
||||
return filePath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -275,6 +275,100 @@ describe("RooIgnoreController", () => {
|
|||
expect(controller.validateCommand("npm install")).toBeUndefined()
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests validation of shell redirections and command substitutions
|
||||
*/
|
||||
it("should block shell redirections that read ignored files", () => {
|
||||
// Input redirection
|
||||
expect(controller.validateCommand("wc -l < node_modules/package.json")).toBe("node_modules/package.json")
|
||||
expect(controller.validateCommand("sort < secrets/api-keys.json")).toBe("secrets/api-keys.json")
|
||||
expect(controller.validateCommand("grep pattern <.git/config")).toBe(".git/config")
|
||||
|
||||
// Should allow non-ignored files
|
||||
expect(controller.validateCommand("wc -l < README.md")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should block command substitutions that read ignored files", () => {
|
||||
// $() command substitution
|
||||
expect(controller.validateCommand("echo $(cat node_modules/package.json)")).toBe(
|
||||
"node_modules/package.json",
|
||||
)
|
||||
expect(controller.validateCommand("result=$(head secrets/api-keys.json)")).toBe("secrets/api-keys.json")
|
||||
|
||||
// Backtick command substitution
|
||||
expect(controller.validateCommand("echo `cat .git/config`")).toBe(".git/config")
|
||||
expect(controller.validateCommand("data=`tail error.log`")).toBe("error.log")
|
||||
|
||||
// Process substitution
|
||||
expect(controller.validateCommand("diff <(cat node_modules/index.js) file2")).toBe("node_modules/index.js")
|
||||
|
||||
// Should allow non-ignored files
|
||||
expect(controller.validateCommand("echo $(cat README.md)")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should block piped commands that read ignored files", () => {
|
||||
// Commands in pipelines
|
||||
expect(controller.validateCommand("cat node_modules/package.json | grep version")).toBe(
|
||||
"node_modules/package.json",
|
||||
)
|
||||
expect(controller.validateCommand("echo test | tee secrets/output.log; cat secrets/output.log")).toBe(
|
||||
"secrets/output.log",
|
||||
)
|
||||
expect(controller.validateCommand("ls && head .git/config")).toBe(".git/config")
|
||||
|
||||
// Should allow non-ignored files in pipelines
|
||||
expect(controller.validateCommand("cat README.md | grep title")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should detect additional file reading commands", () => {
|
||||
// Additional Unix utilities
|
||||
expect(controller.validateCommand("nl node_modules/package.json")).toBe("node_modules/package.json")
|
||||
expect(controller.validateCommand("tac .git/config")).toBe(".git/config")
|
||||
expect(controller.validateCommand("strings secrets/binary.dat")).toBe("secrets/binary.dat")
|
||||
expect(controller.validateCommand("hexdump error.log")).toBe("error.log")
|
||||
expect(controller.validateCommand("od -c node_modules/index.js")).toBe("node_modules/index.js")
|
||||
|
||||
// Compressed file readers
|
||||
expect(controller.validateCommand("zcat secrets/data.gz")).toBe("secrets/data.gz")
|
||||
expect(controller.validateCommand("bzcat node_modules/archive.bz2")).toBe("node_modules/archive.bz2")
|
||||
|
||||
// File comparison utilities
|
||||
expect(controller.validateCommand("diff .git/config file2")).toBe(".git/config")
|
||||
expect(controller.validateCommand("cmp secrets/key1 secrets/key2")).toBe("secrets/key1")
|
||||
|
||||
// Windows/PowerShell commands
|
||||
expect(controller.validateCommand("findstr pattern node_modules/package.json")).toBe(
|
||||
"node_modules/package.json",
|
||||
)
|
||||
expect(controller.validateCommand("fc .git/config file2")).toBe(".git/config")
|
||||
})
|
||||
|
||||
it("should handle complex command patterns", () => {
|
||||
// Commands with quotes
|
||||
expect(controller.validateCommand('cat "node_modules/package.json"')).toBe("node_modules/package.json")
|
||||
expect(controller.validateCommand("cat 'secrets/api-keys.json'")).toBe("secrets/api-keys.json")
|
||||
|
||||
// Multiple files in one command
|
||||
expect(controller.validateCommand("cat README.md node_modules/index.js")).toBe("node_modules/index.js")
|
||||
|
||||
// Nested command substitutions
|
||||
expect(controller.validateCommand("echo $(echo $(cat .git/config))")).toBe(".git/config")
|
||||
|
||||
// Mixed patterns
|
||||
expect(controller.validateCommand("cat < node_modules/data.txt | grep pattern")).toBe(
|
||||
"node_modules/data.txt",
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle xargs and find commands that might read files", () => {
|
||||
// xargs with file reading commands
|
||||
expect(controller.validateCommand("find . -name '*.json' | xargs cat")).toBeUndefined() // Can't determine specific files
|
||||
expect(controller.validateCommand("echo node_modules/package.json | xargs cat")).toBeUndefined() // File path is in stdin, not command
|
||||
|
||||
// find with -exec
|
||||
expect(controller.validateCommand("find . -name 'package.json' -exec cat {} \\;")).toBeUndefined() // Can't determine specific files
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests behavior when no .rooignore exists
|
||||
*/
|
||||
|
|
@ -287,6 +381,8 @@ describe("RooIgnoreController", () => {
|
|||
// All commands should be allowed
|
||||
expect(emptyController.validateCommand("cat node_modules/package.json")).toBeUndefined()
|
||||
expect(emptyController.validateCommand("grep pattern .git/config")).toBeUndefined()
|
||||
expect(emptyController.validateCommand("echo $(cat secrets/file)")).toBeUndefined()
|
||||
expect(emptyController.validateCommand("wc < .git/config")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue