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:
250
src/constants/dangerous-commands.ts
Normal file
250
src/constants/dangerous-commands.ts
Normal 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.",
|
||||
};
|
||||
107
src/services/dangerous-command-blocker.ts
Normal file
107
src/services/dangerous-command-blocker.ts
Normal 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",
|
||||
];
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user