mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Adding allow-list for auto-executable commands
This commit is contained in:
parent
10a1388017
commit
90036892c8
4 changed files with 420 additions and 26 deletions
319
package-lock.json
generated
319
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -144,10 +144,12 @@
|
|||
"@typescript-eslint/parser": "^7.11.0",
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
"@vscode/test-electron": "^2.4.0",
|
||||
"esbuild": "^0.24.0",
|
||||
"eslint": "^8.57.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.4.5"
|
||||
"typescript": "^5.4.5",
|
||||
"jest": "^29.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
||||
|
|
@ -164,11 +166,9 @@
|
|||
"default-shell": "^2.2.0",
|
||||
"delay": "^6.0.0",
|
||||
"diff": "^5.2.0",
|
||||
"esbuild": "^0.24.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"globby": "^14.0.2",
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"jest": "^29.7.0",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"openai": "^4.61.0",
|
||||
|
|
|
|||
|
|
@ -56,6 +56,16 @@ type UserContent = Array<
|
|||
Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam
|
||||
>
|
||||
|
||||
const ALLOWED_AUTO_EXECUTE_COMMANDS = [
|
||||
'npm',
|
||||
'npx',
|
||||
'tsc',
|
||||
'git log',
|
||||
'git diff',
|
||||
'git show',
|
||||
'list'
|
||||
] as const
|
||||
|
||||
export class Cline {
|
||||
readonly taskId: string
|
||||
api: ApiHandler
|
||||
|
|
@ -220,6 +230,25 @@ export class Cline {
|
|||
}
|
||||
}
|
||||
|
||||
protected isAllowedCommand(command?: string): boolean {
|
||||
if (!command) {
|
||||
return false;
|
||||
}
|
||||
// Check for command chaining characters
|
||||
if (command.includes('&&') ||
|
||||
command.includes(';') ||
|
||||
command.includes('||') ||
|
||||
command.includes('|') ||
|
||||
command.includes('$(') ||
|
||||
command.includes('`')) {
|
||||
return false;
|
||||
}
|
||||
const trimmedCommand = command.trim().toLowerCase();
|
||||
return ALLOWED_AUTO_EXECUTE_COMMANDS.some(prefix =>
|
||||
trimmedCommand.startsWith(prefix.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
// Communicate with webview
|
||||
|
||||
// partial has three valid states true (partial message), false (completion of partial message), undefined (individual complete message)
|
||||
|
|
@ -1507,7 +1536,7 @@ export class Cline {
|
|||
const command: string | undefined = block.params.command
|
||||
try {
|
||||
if (block.partial) {
|
||||
if (this.alwaysAllowExecute) {
|
||||
if (this.alwaysAllowExecute && this.isAllowedCommand(command)) {
|
||||
await this.say("command", command, undefined, block.partial)
|
||||
} else {
|
||||
await this.ask("command", removeClosingTag("command", command), block.partial).catch(
|
||||
|
|
@ -1524,7 +1553,7 @@ export class Cline {
|
|||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
const didApprove = this.alwaysAllowExecute || (await askApproval("command", command))
|
||||
const didApprove = (this.alwaysAllowExecute && this.isAllowedCommand(command)) || (await askApproval("command", command))
|
||||
if (!didApprove) {
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -319,4 +319,92 @@ describe('Cline', () => {
|
|||
// The write operation would require approval in actual implementation
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAllowedCommand', () => {
|
||||
let cline: any
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a more complete mock provider
|
||||
const mockProvider = {
|
||||
context: {
|
||||
globalStorageUri: { fsPath: '/mock/path' }
|
||||
},
|
||||
postStateToWebview: jest.fn(),
|
||||
postMessageToWebview: jest.fn(),
|
||||
updateTaskHistory: jest.fn()
|
||||
}
|
||||
|
||||
// Mock the required dependencies
|
||||
const mockApiConfig = {
|
||||
getModel: () => ({
|
||||
id: 'claude-3-sonnet',
|
||||
info: { supportsComputerUse: true }
|
||||
})
|
||||
}
|
||||
|
||||
// Create test instance with mocked constructor params
|
||||
cline = new Cline(
|
||||
mockProvider as any,
|
||||
mockApiConfig as any,
|
||||
undefined, // customInstructions
|
||||
false, // alwaysAllowReadOnly
|
||||
false, // alwaysAllowWrite
|
||||
false, // alwaysAllowExecute
|
||||
'test task' // task
|
||||
)
|
||||
|
||||
// Mock internal methods that are called during initialization
|
||||
cline.initiateTaskLoop = jest.fn()
|
||||
cline.say = jest.fn()
|
||||
cline.addToClineMessages = jest.fn()
|
||||
cline.overwriteClineMessages = jest.fn()
|
||||
cline.addToApiConversationHistory = jest.fn()
|
||||
cline.overwriteApiConversationHistory = jest.fn()
|
||||
})
|
||||
|
||||
test('returns true for allowed commands', () => {
|
||||
expect(cline.isAllowedCommand('npm install')).toBe(true)
|
||||
expect(cline.isAllowedCommand('npx create-react-app')).toBe(true)
|
||||
expect(cline.isAllowedCommand('tsc --watch')).toBe(true)
|
||||
expect(cline.isAllowedCommand('git log --oneline')).toBe(true)
|
||||
expect(cline.isAllowedCommand('git diff main')).toBe(true)
|
||||
})
|
||||
|
||||
test('returns true regardless of case or whitespace', () => {
|
||||
expect(cline.isAllowedCommand('NPM install')).toBe(true)
|
||||
expect(cline.isAllowedCommand(' npm install')).toBe(true)
|
||||
expect(cline.isAllowedCommand('GIT DIFF')).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for non-allowed commands', () => {
|
||||
expect(cline.isAllowedCommand('rm -rf /')).toBe(false)
|
||||
expect(cline.isAllowedCommand('git push')).toBe(false)
|
||||
expect(cline.isAllowedCommand('git commit')).toBe(false)
|
||||
expect(cline.isAllowedCommand('curl http://example.com')).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false for undefined or empty commands', () => {
|
||||
expect(cline.isAllowedCommand()).toBe(false)
|
||||
expect(cline.isAllowedCommand('')).toBe(false)
|
||||
expect(cline.isAllowedCommand(' ')).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false for commands with chaining operators', () => {
|
||||
const maliciousCommands = [
|
||||
'npm install && rm -rf /',
|
||||
'git status; dangerous-command',
|
||||
'git log || evil-script',
|
||||
'git status | malicious-pipe',
|
||||
'git log $(evil-command)',
|
||||
'git status `rm -rf /`',
|
||||
'npm install && echo "malicious"',
|
||||
'git status; curl http://evil.com',
|
||||
'tsc --watch || wget malware',
|
||||
];
|
||||
|
||||
maliciousCommands.forEach(cmd => {
|
||||
expect(cline.isAllowedCommand(cmd)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue