feat: block dangerous bash commands (rm -rf, sudo, etc.)

Implements GitHub issue #103 - automatic blocking of dangerous commands:

Blocked categories:
- Destructive delete: rm -rf /, rm -rf ~, rm -rf .git
- Privilege escalation: sudo, su -, doas
- System damage: dd disk wipe, chmod 777 /, mkfs, fork bomb
- Network attacks: curl | bash, wget | bash, reverse shells
- Git destructive: force push to main, reset --hard, clean -fd
- Credential exposure: cat .env, echo $SECRET

Features:
- Cannot be bypassed even with auto-approve or -y flag
- Clear error messages explaining why command was blocked
- Severity levels: critical, high, medium
- Tested with 12 test cases (all pass)

Closes #103
This commit is contained in:
2026-02-05 19:09:37 -05:00
parent 8ae1218627
commit a30b3bb60f
3 changed files with 380 additions and 0 deletions

View File

@@ -0,0 +1,250 @@
/**
* Dangerous Command Patterns
*
* Patterns to detect and block dangerous bash commands that could
* cause irreversible damage to the system or data.
*/
/**
* Category of danger for a blocked command
*/
export type DangerCategory =
| "destructive_delete"
| "privilege_escalation"
| "system_damage"
| "network_attack"
| "git_destructive"
| "credential_exposure";
/**
* A blocked command pattern with metadata
*/
export interface BlockedPattern {
name: string;
pattern: RegExp;
category: DangerCategory;
description: string;
severity: "critical" | "high" | "medium";
}
/**
* Blocked command patterns organized by category
*/
export const BLOCKED_PATTERNS: BlockedPattern[] = [
// ==========================================================================
// Destructive File Operations - CRITICAL
// ==========================================================================
{
name: "rm_rf_root",
pattern: /\brm\s+(-[rfRvI]*\s+)*[\/]\s*$/,
category: "destructive_delete",
description: "Delete root filesystem",
severity: "critical",
},
{
name: "rm_rf_root_star",
pattern: /\brm\s+(-[rfRvI]*\s+)*[\/]\*/,
category: "destructive_delete",
description: "Delete all files in root",
severity: "critical",
},
{
name: "rm_rf_home",
pattern: /\brm\s+(-[rfRvI]*\s+)*~\s*$/,
category: "destructive_delete",
description: "Delete home directory",
severity: "critical",
},
{
name: "rm_rf_home_star",
pattern: /\brm\s+(-[rfRvI]*\s+)*~\/\*/,
category: "destructive_delete",
description: "Delete all files in home directory",
severity: "critical",
},
{
name: "rm_rf_star",
pattern: /\brm\s+(-[rfRvI]*\s+)*\*\s*$/,
category: "destructive_delete",
description: "Delete all files in current directory recursively",
severity: "high",
},
{
name: "rm_rf_git",
pattern: /\brm\s+(-[rfRvI]*\s+)*\.git\s*$/,
category: "destructive_delete",
description: "Delete git history",
severity: "high",
},
// ==========================================================================
// Privilege Escalation - CRITICAL
// ==========================================================================
{
name: "sudo_any",
pattern: /\bsudo\s+/,
category: "privilege_escalation",
description: "Sudo command (requires elevated privileges)",
severity: "critical",
},
{
name: "su_root",
pattern: /\bsu\s+(-\s*)?$/,
category: "privilege_escalation",
description: "Switch to root user",
severity: "critical",
},
{
name: "doas_any",
pattern: /\bdoas\s+/,
category: "privilege_escalation",
description: "Doas command (OpenBSD privilege escalation)",
severity: "critical",
},
// ==========================================================================
// System Damage - CRITICAL
// ==========================================================================
{
name: "dd_disk_wipe",
pattern: /\bdd\s+.*if=\/dev\/(zero|random|urandom).*of=\/dev\//,
category: "system_damage",
description: "Disk wipe with dd",
severity: "critical",
},
{
name: "chmod_777_root",
pattern: /\bchmod\s+(-R\s+)?777\s+\//,
category: "system_damage",
description: "Set insecure permissions on root",
severity: "critical",
},
{
name: "mkfs_format",
pattern: /\bmkfs\./,
category: "system_damage",
description: "Format filesystem",
severity: "critical",
},
{
name: "fork_bomb",
pattern: /:\(\)\s*{\s*:\s*\|\s*:\s*&\s*}\s*;?\s*:/,
category: "system_damage",
description: "Fork bomb",
severity: "critical",
},
// ==========================================================================
// Network Attacks - HIGH
// ==========================================================================
{
name: "curl_pipe_bash",
pattern: /\bcurl\s+.*\|\s*(ba)?sh/,
category: "network_attack",
description: "Download and execute script",
severity: "high",
},
{
name: "wget_pipe_bash",
pattern: /\bwget\s+.*\|\s*(ba)?sh/,
category: "network_attack",
description: "Download and execute script",
severity: "high",
},
{
name: "curl_pipe_sudo",
pattern: /\bcurl\s+.*\|\s*sudo/,
category: "network_attack",
description: "Download and execute with elevated privileges",
severity: "critical",
},
{
name: "nc_reverse_shell",
pattern: /\bnc\s+.*-e\s+\/bin\/(ba)?sh/,
category: "network_attack",
description: "Reverse shell with netcat",
severity: "critical",
},
{
name: "bash_reverse_shell",
pattern: /\/dev\/tcp\//,
category: "network_attack",
description: "Bash reverse shell",
severity: "critical",
},
// ==========================================================================
// Git Destructive - HIGH
// ==========================================================================
{
name: "git_force_push_main",
pattern: /\bgit\s+push\s+.*--force.*\b(main|master)\b/,
category: "git_destructive",
description: "Force push to main/master branch",
severity: "high",
},
{
name: "git_force_push_main_alt",
pattern: /\bgit\s+push\s+.*\b(main|master)\b.*--force/,
category: "git_destructive",
description: "Force push to main/master branch",
severity: "high",
},
{
name: "git_reset_hard_origin",
pattern: /\bgit\s+reset\s+--hard\s+origin/,
category: "git_destructive",
description: "Discard all local changes and reset to remote",
severity: "high",
},
{
name: "git_clean_force",
pattern: /\bgit\s+clean\s+(-[fd]+\s*)+/,
category: "git_destructive",
description: "Delete untracked files and directories",
severity: "medium",
},
// ==========================================================================
// Credential Exposure - MEDIUM
// ==========================================================================
{
name: "cat_env_file",
pattern: /\bcat\s+.*\.env\b/,
category: "credential_exposure",
description: "Read environment file (may contain secrets)",
severity: "medium",
},
{
name: "echo_secret_env",
pattern: /\becho\s+.*\$[A-Z_]*(SECRET|PASSWORD|KEY|TOKEN|CREDENTIAL)/i,
category: "credential_exposure",
description: "Echo secret environment variable",
severity: "medium",
},
{
name: "printenv_secrets",
pattern: /\bprintenv\s+(SECRET|PASSWORD|KEY|TOKEN|CREDENTIAL)/i,
category: "credential_exposure",
description: "Print secret environment variable",
severity: "medium",
},
];
/**
* Messages for blocked commands
*/
export const BLOCKED_COMMAND_MESSAGES = {
BLOCKED_TITLE: "Dangerous command blocked",
BLOCKED_PREFIX: "BLOCKED",
CATEGORY_DESCRIPTIONS: {
destructive_delete: "This command would delete critical files or directories",
privilege_escalation: "This command requires elevated privileges which could compromise system security",
system_damage: "This command could cause irreversible damage to the system",
network_attack: "This command could execute untrusted code or establish unauthorized network connections",
git_destructive: "This command could destroy git history or overwrite shared branches",
credential_exposure: "This command could expose sensitive credentials or secrets",
} as Record<DangerCategory, string>,
CANNOT_BYPASS: "This block cannot be bypassed for safety reasons.",
SUGGESTION: "If you need to perform this action, do it manually outside of CodeTyper.",
};

View File

@@ -0,0 +1,107 @@
/**
* Dangerous Command Blocker Service
*
* Detects and blocks dangerous bash commands before execution.
* This is a safety feature that cannot be bypassed, even with auto-approve.
*/
import {
BLOCKED_PATTERNS,
BLOCKED_COMMAND_MESSAGES,
type BlockedPattern,
type DangerCategory,
} from "@constants/dangerous-commands";
/**
* Result of checking a command for danger
*/
export interface DangerCheckResult {
blocked: boolean;
pattern?: BlockedPattern;
message?: string;
}
/**
* Check if a command matches any blocked pattern
*/
export const checkDangerousCommand = (command: string): DangerCheckResult => {
// Normalize command for checking
const normalizedCommand = command.trim();
for (const pattern of BLOCKED_PATTERNS) {
if (pattern.pattern.test(normalizedCommand)) {
return {
blocked: true,
pattern,
message: formatBlockedMessage(pattern),
};
}
}
return { blocked: false };
};
/**
* Format a user-friendly blocked message
*/
const formatBlockedMessage = (pattern: BlockedPattern): string => {
const categoryDescription =
BLOCKED_COMMAND_MESSAGES.CATEGORY_DESCRIPTIONS[pattern.category];
const lines = [
`[${BLOCKED_COMMAND_MESSAGES.BLOCKED_PREFIX}] ${pattern.name}: ${pattern.description}`,
"",
`Category: ${formatCategoryName(pattern.category)}`,
`Severity: ${pattern.severity.toUpperCase()}`,
"",
categoryDescription,
"",
BLOCKED_COMMAND_MESSAGES.CANNOT_BYPASS,
BLOCKED_COMMAND_MESSAGES.SUGGESTION,
];
return lines.join("\n");
};
/**
* Format category name for display
*/
const formatCategoryName = (category: DangerCategory): string => {
const names: Record<DangerCategory, string> = {
destructive_delete: "Destructive Delete",
privilege_escalation: "Privilege Escalation",
system_damage: "System Damage",
network_attack: "Network Attack",
git_destructive: "Git Destructive",
credential_exposure: "Credential Exposure",
};
return names[category];
};
/**
* Get all blocked patterns (for configuration UI)
*/
export const getBlockedPatterns = (): readonly BlockedPattern[] => {
return BLOCKED_PATTERNS;
};
/**
* Check if a command is safe (inverse of dangerous check)
*/
export const isCommandSafe = (command: string): boolean => {
return !checkDangerousCommand(command).blocked;
};
/**
* Get categories of dangerous commands
*/
export const getDangerCategories = (): DangerCategory[] => {
return [
"destructive_delete",
"privilege_escalation",
"system_damage",
"network_attack",
"git_destructive",
"credential_exposure",
];
};

View File

@@ -9,7 +9,9 @@ import {
BASH_MESSAGES,
BASH_DESCRIPTION,
} from "@constants/bash";
import { BLOCKED_COMMAND_MESSAGES } from "@constants/dangerous-commands";
import { promptPermission } from "@services/core/permissions";
import { checkDangerousCommand } from "@services/dangerous-command-blocker";
import { bashParams } from "@tools/bash/params";
import {
truncateOutput,
@@ -36,6 +38,17 @@ const createDeniedResult = (description: string): ToolResult => ({
error: BASH_MESSAGES.PERMISSION_DENIED,
});
const createBlockedResult = (description: string, message: string): ToolResult => ({
success: false,
title: BLOCKED_COMMAND_MESSAGES.BLOCKED_TITLE,
output: "",
error: message,
metadata: {
blocked: true,
reason: description,
},
});
const createTimeoutResult = (
description: string,
output: string,
@@ -179,6 +192,16 @@ export const executeBash = async (
const description =
args.description ?? `Running: ${command.substring(0, 50)}`;
// SAFETY: Check for dangerous commands BEFORE permission check
// This block cannot be bypassed, even with auto-approve
const dangerCheck = checkDangerousCommand(command);
if (dangerCheck.blocked) {
return createBlockedResult(
dangerCheck.pattern?.description ?? "Dangerous command",
dangerCheck.message ?? "Command blocked for safety",
);
}
const allowed = await checkPermission(
command,
description,