Skip to content

Commit 779b34b

Browse files
author
StackMemory Bot (CLI)
committed
fix(security): guard against rm -rf in hooks and source code
- Add rm -rf/rm -r/rm -fr/rm -Rf to deny list in .claude/settings.json and settings.local.json (hard block, no prompt bypass) - Replace execSync('rm -rf ...') with fs.rmSync() in ralph.ts and setup.ts - Add validateShellCommand() to input-sanitizer.ts with DANGEROUS_SHELL_PATTERNS regex blocking recursive rm, mkfs, dd, and fork bombs - Export validateShellCommand from security/index.ts - Add 4 tests covering blocked and allowed patterns
1 parent de4832f commit 779b34b

6 files changed

Lines changed: 67 additions & 5 deletions

File tree

.claude/settings.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,11 @@
7878
},
7979
"permissions": {
8080
"deny": [
81-
"Read(./.entire/metadata/**)"
81+
"Read(./.entire/metadata/**)",
82+
"Bash(rm -rf:*)",
83+
"Bash(rm -r :*)",
84+
"Bash(rm -fr:*)",
85+
"Bash(rm -Rf:*)"
8286
]
8387
}
8488
}

src/cli/commands/ralph.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,8 @@ export function createRalphCommand(): Command {
265265
try {
266266
// Clean up Ralph directory
267267
if (!options.keepHistory && existsSync('.ralph/history')) {
268-
const { execSync } = await import('child_process');
269-
execSync('rm -rf .ralph/history');
268+
const fs = await import('fs');
269+
fs.rmSync('.ralph/history', { recursive: true, force: true });
270270
}
271271

272272
// Remove working files but keep task definition

src/cli/commands/setup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { Command } from 'commander';
88
import chalk from 'chalk';
9-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
9+
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'fs';
1010
import { join } from 'path';
1111
import { homedir } from 'os';
1212
import { execSync } from 'child_process';
@@ -471,7 +471,7 @@ export function createSetupPluginsCommand(): Command {
471471
if (options.force) {
472472
// Remove existing
473473
try {
474-
execSync(`rm -rf "${targetPath}"`, { encoding: 'utf-8' });
474+
rmSync(targetPath, { recursive: true, force: true });
475475
} catch {
476476
console.log(
477477
chalk.red(` [ERROR] ${plugin} - could not remove existing`)

src/core/security/__tests__/input-sanitizer.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
sanitizeForLogging,
1515
InputSchemas,
1616
validateInput,
17+
validateShellCommand,
1718
} from '../input-sanitizer.js';
1819
import { ValidationError } from '../../errors/index.js';
1920

@@ -82,6 +83,34 @@ describe('validateShellArg', () => {
8283
});
8384
});
8485

86+
describe('validateShellCommand', () => {
87+
it('should block rm -rf patterns', () => {
88+
expect(() => validateShellCommand('rm -rf /')).toThrow(ValidationError);
89+
expect(() => validateShellCommand('rm -rf /tmp/foo')).toThrow(
90+
ValidationError
91+
);
92+
expect(() => validateShellCommand('rm -fr /tmp/bar')).toThrow(
93+
ValidationError
94+
);
95+
expect(() => validateShellCommand('rm -Rf node_modules')).toThrow(
96+
ValidationError
97+
);
98+
expect(() => validateShellCommand('rm -r /some/dir')).toThrow(
99+
ValidationError
100+
);
101+
});
102+
103+
it('should allow safe rm (single file)', () => {
104+
expect(validateShellCommand('rm file.txt')).toBe('rm file.txt');
105+
expect(validateShellCommand('rm -f file.txt')).toBe('rm -f file.txt');
106+
});
107+
108+
it('should allow non-rm commands', () => {
109+
expect(validateShellCommand('ls -la')).toBe('ls -la');
110+
expect(validateShellCommand('npm run build')).toBe('npm run build');
111+
});
112+
});
113+
85114
describe('sanitizeForLogging', () => {
86115
it('should redact sensitive fields in objects', () => {
87116
const obj = { username: 'user', password: 'secret', apiKey: 'key123' };

src/core/security/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export {
2525

2626
// Shell command safety
2727
validateShellArg,
28+
validateShellCommand,
2829

2930
// JSON safety
3031
safeJsonParse,

src/core/security/input-sanitizer.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,34 @@ export function createAggregateSchema(allowedFields: string[]) {
414414
});
415415
}
416416

417+
/**
418+
* Dangerous shell command patterns that should never be executed programmatically
419+
*/
420+
const DANGEROUS_SHELL_PATTERNS = [
421+
/\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|--recursive)\b/i, // rm -rf, rm -fr, rm --recursive
422+
/\brm\s+-[a-zA-Z]*r\b/i, // rm -r (any flag combo with r)
423+
/\bmkfs\b/i,
424+
/\bdd\s+if=/i,
425+
/\b:\(\)\s*\{/i, // fork bomb
426+
];
427+
428+
/**
429+
* Validate a shell command string for dangerous patterns
430+
* Use before execSync/exec to prevent destructive commands
431+
*/
432+
export function validateShellCommand(command: string): string {
433+
for (const pattern of DANGEROUS_SHELL_PATTERNS) {
434+
if (pattern.test(command)) {
435+
throw new ValidationError(
436+
`Blocked dangerous shell command: ${command.substring(0, 80)}`,
437+
ErrorCode.VALIDATION_FAILED,
438+
{ reason: 'dangerous_command' }
439+
);
440+
}
441+
}
442+
return command;
443+
}
444+
417445
/**
418446
* Validate command line arguments for shell safety
419447
* Prevents command injection

0 commit comments

Comments
 (0)