security: add dangerous command detection and blocking
- Add src/security/dangerous-commands.ts module that detects: - Critical: rm -rf /, fork bombs, direct writes to block devices - High: curl|sh, wget|sh, git push --force, sudo rm, xargs rm - Medium: rm -rf, git reset --hard, killall, shutdown - Hook into bash-tools.exec.ts to block dangerous commands - Checks run before any execution or approval flow - Default block level is 'high' (critical + high patterns) This prevents the agent from executing catastrophic commands even if they would otherwise pass the allowlist.
This commit is contained in:
parent
67abc6a01b
commit
0be272ed70
@ -56,6 +56,10 @@ import { listNodes, resolveNodeIdFromList } from "./tools/nodes-utils.js";
|
||||
import { getShellConfig, sanitizeBinaryOutput } from "./shell-utils.js";
|
||||
import { buildCursorPositionResponse, stripDsrRequests } from "./pty-dsr.js";
|
||||
import { parseAgentSessionKey, resolveAgentIdFromSessionKey } from "../routing/session-key.js";
|
||||
import {
|
||||
detectDangerousCommand,
|
||||
formatDangerousCommandError,
|
||||
} from "../security/dangerous-commands.js";
|
||||
|
||||
const DEFAULT_MAX_OUTPUT = clampNumber(
|
||||
readEnvInt("PI_BASH_MAX_OUTPUT_CHARS"),
|
||||
@ -755,6 +759,12 @@ export function createExecTool(
|
||||
throw new Error("Provide a command to start.");
|
||||
}
|
||||
|
||||
// SECURITY: Check for dangerous command patterns before any execution
|
||||
const dangerousMatch = detectDangerousCommand(params.command, "high");
|
||||
if (dangerousMatch) {
|
||||
throw new Error(formatDangerousCommandError(dangerousMatch));
|
||||
}
|
||||
|
||||
const maxOutput = DEFAULT_MAX_OUTPUT;
|
||||
const pendingMaxOutput = DEFAULT_PENDING_MAX_OUTPUT;
|
||||
const warnings: string[] = [];
|
||||
|
||||
197
src/security/dangerous-commands.ts
Normal file
197
src/security/dangerous-commands.ts
Normal file
@ -0,0 +1,197 @@
|
||||
/**
|
||||
* SECURITY: Dangerous Command Detection
|
||||
*
|
||||
* Blocks commands that are inherently dangerous regardless of allowlist status.
|
||||
* These patterns are blocked at the lowest level before any execution.
|
||||
*/
|
||||
|
||||
export interface DangerousCommandMatch {
|
||||
pattern: string;
|
||||
reason: string;
|
||||
severity: "critical" | "high" | "medium";
|
||||
}
|
||||
|
||||
// Critical: Commands that can cause catastrophic damage
|
||||
const CRITICAL_PATTERNS: Array<{ regex: RegExp; reason: string }> = [
|
||||
{
|
||||
regex: /\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?(-[a-zA-Z]*r[a-zA-Z]*\s+)?[\/~]\s*$/i,
|
||||
reason: "Recursive delete of root or home directory",
|
||||
},
|
||||
{
|
||||
regex: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+)?(-[a-zA-Z]*f[a-zA-Z]*\s+)?[\/~]\s*$/i,
|
||||
reason: "Recursive delete of root or home directory",
|
||||
},
|
||||
{
|
||||
regex: /\brm\s+-rf\s+\/(?!\w)/i,
|
||||
reason: "Recursive forced delete from root",
|
||||
},
|
||||
{
|
||||
regex: /\brm\s+-fr\s+\/(?!\w)/i,
|
||||
reason: "Recursive forced delete from root",
|
||||
},
|
||||
{
|
||||
regex: />\s*\/dev\/sd[a-z]/i,
|
||||
reason: "Direct write to block device",
|
||||
},
|
||||
{
|
||||
regex: /\bdd\s+.*\bof=\/dev\/sd[a-z]/i,
|
||||
reason: "Direct write to block device with dd",
|
||||
},
|
||||
{
|
||||
regex: /\bmkfs\b/i,
|
||||
reason: "Filesystem format command",
|
||||
},
|
||||
{
|
||||
regex: /\b:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/,
|
||||
reason: "Fork bomb detected",
|
||||
},
|
||||
];
|
||||
|
||||
// High: Commands that can compromise security or cause significant damage
|
||||
const HIGH_PATTERNS: Array<{ regex: RegExp; reason: string }> = [
|
||||
{
|
||||
regex: /\bcurl\s+.*\|\s*(ba)?sh\b/i,
|
||||
reason: "Piping remote content directly to shell",
|
||||
},
|
||||
{
|
||||
regex: /\bwget\s+.*\|\s*(ba)?sh\b/i,
|
||||
reason: "Piping remote content directly to shell",
|
||||
},
|
||||
{
|
||||
regex: /\bcurl\s+.*\|\s*sudo\b/i,
|
||||
reason: "Piping remote content to sudo",
|
||||
},
|
||||
{
|
||||
regex: /\bwget\s+.*\|\s*sudo\b/i,
|
||||
reason: "Piping remote content to sudo",
|
||||
},
|
||||
{
|
||||
regex: /\beval\s+.*\$\(curl\b/i,
|
||||
reason: "Eval of remote content",
|
||||
},
|
||||
{
|
||||
regex: /\beval\s+.*\$\(wget\b/i,
|
||||
reason: "Eval of remote content",
|
||||
},
|
||||
{
|
||||
regex: /\bgit\s+push\s+.*--force\b/i,
|
||||
reason: "Force push can destroy remote history",
|
||||
},
|
||||
{
|
||||
regex: /\bgit\s+push\s+-f\b/i,
|
||||
reason: "Force push can destroy remote history",
|
||||
},
|
||||
{
|
||||
regex: /\bchmod\s+(-[a-zA-Z]*\s+)?777\s+\//i,
|
||||
reason: "Setting world-writable permissions on system paths",
|
||||
},
|
||||
{
|
||||
regex: /\bchown\s+.*\s+\/(?:etc|usr|bin|sbin|lib|var)\b/i,
|
||||
reason: "Changing ownership of system directories",
|
||||
},
|
||||
{
|
||||
regex: />\s*\/etc\//i,
|
||||
reason: "Direct write to /etc",
|
||||
},
|
||||
{
|
||||
regex: /\bsudo\s+rm\b/i,
|
||||
reason: "Elevated delete command",
|
||||
},
|
||||
{
|
||||
regex: /\|\s*rm\b/i,
|
||||
reason: "Piping to rm command",
|
||||
},
|
||||
{
|
||||
regex: /\bxargs\s+.*\brm\b/i,
|
||||
reason: "Using xargs with rm",
|
||||
},
|
||||
];
|
||||
|
||||
// Medium: Commands that warrant caution but may have legitimate uses
|
||||
const MEDIUM_PATTERNS: Array<{ regex: RegExp; reason: string }> = [
|
||||
{
|
||||
regex: /\brm\s+-rf?\b/i,
|
||||
reason: "Recursive delete (verify target carefully)",
|
||||
},
|
||||
{
|
||||
regex: /\bgit\s+reset\s+--hard\b/i,
|
||||
reason: "Hard reset discards uncommitted changes",
|
||||
},
|
||||
{
|
||||
regex: /\bgit\s+clean\s+-fd/i,
|
||||
reason: "Clean removes untracked files",
|
||||
},
|
||||
{
|
||||
regex: /\bkillall\b/i,
|
||||
reason: "Kills all processes matching name",
|
||||
},
|
||||
{
|
||||
regex: /\bpkill\s+-9\b/i,
|
||||
reason: "Force kills processes",
|
||||
},
|
||||
{
|
||||
regex: /\bshutdown\b/i,
|
||||
reason: "System shutdown command",
|
||||
},
|
||||
{
|
||||
regex: /\breboot\b/i,
|
||||
reason: "System reboot command",
|
||||
},
|
||||
{
|
||||
regex: />\s*\/dev\/null\s*2>&1\s*&/i,
|
||||
reason: "Background process with suppressed output",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if a command matches any dangerous patterns.
|
||||
* Returns the match details if dangerous, null if safe.
|
||||
*
|
||||
* @param command The shell command to check
|
||||
* @param blockLevel Minimum severity to block: "critical" blocks only critical,
|
||||
* "high" blocks critical+high, "medium" blocks all
|
||||
*/
|
||||
export function detectDangerousCommand(
|
||||
command: string,
|
||||
blockLevel: "critical" | "high" | "medium" = "high"
|
||||
): DangerousCommandMatch | null {
|
||||
const normalized = command.trim();
|
||||
|
||||
// Always check critical patterns
|
||||
for (const { regex, reason } of CRITICAL_PATTERNS) {
|
||||
if (regex.test(normalized)) {
|
||||
return { pattern: regex.source, reason, severity: "critical" };
|
||||
}
|
||||
}
|
||||
|
||||
// Check high patterns if blockLevel is high or medium
|
||||
if (blockLevel === "high" || blockLevel === "medium") {
|
||||
for (const { regex, reason } of HIGH_PATTERNS) {
|
||||
if (regex.test(normalized)) {
|
||||
return { pattern: regex.source, reason, severity: "high" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check medium patterns only if blockLevel is medium
|
||||
if (blockLevel === "medium") {
|
||||
for (const { regex, reason } of MEDIUM_PATTERNS) {
|
||||
if (regex.test(normalized)) {
|
||||
return { pattern: regex.source, reason, severity: "medium" };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a user-friendly error message for a blocked command.
|
||||
*/
|
||||
export function formatDangerousCommandError(match: DangerousCommandMatch): string {
|
||||
return (
|
||||
`Command blocked (${match.severity}): ${match.reason}. ` +
|
||||
"This command pattern is restricted for security. " +
|
||||
"If you need to perform this action, do it manually outside of the agent."
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user