From 826cce0257b9aa8f5deb666fd3249b79653bd102 Mon Sep 17 00:00:00 2001 From: dorien-koelemeijer <62866702+dorien-koelemeijer@users.noreply.github.com> Date: Thu, 14 May 2026 12:19:48 +1000 Subject: [PATCH] Prompt injection mitigation: update pattern-based detection (#9198) --- crates/goose/src/security/patterns.rs | 96 +++++++++++++++++++++------ 1 file changed, 75 insertions(+), 21 deletions(-) diff --git a/crates/goose/src/security/patterns.rs b/crates/goose/src/security/patterns.rs index df8173f5..75bef0f4 100644 --- a/crates/goose/src/security/patterns.rs +++ b/crates/goose/src/security/patterns.rs @@ -54,9 +54,9 @@ pub const THREAT_PATTERNS: &[ThreatPattern] = &[ category: ThreatCategory::FileSystemDestruction, }, ThreatPattern { - name: "rm_rf_system", - pattern: r"rm\s+(-[rf]*[rf][rf]*|--recursive|--force)\s+[^\s;|&]*/?(bin|etc|usr|var|sys|proc|dev|boot|lib|opt|srv)(?:/|\s|[;&|]|$)", - description: "Recursive deletion of system directories", + name: "rm_rf_home_or_root", + pattern: r"rm\s+((--[a-zA-Z][a-zA-Z\-]*|--|-[a-zA-Z]+)\s+)*(-[a-zA-Z]*[rR][a-zA-Z]*|--recursive|--dir|-d)(\s+(--[a-zA-Z][a-zA-Z\-]*|--|-[a-zA-Z]+))*\s+['\x22]?(~|\$HOME|\$\{HOME\}|/home|/root)/?(\*)?['\x22]?(\s|[;&|]|$)", + description: "Recursive deletion of home or root directory", risk_level: RiskLevel::Critical, category: ThreatCategory::FileSystemDestruction, }, @@ -242,14 +242,6 @@ pub const THREAT_PATTERNS: &[ThreatPattern] = &[ risk_level: RiskLevel::Medium, category: ThreatCategory::CommandInjection, }, - ThreatPattern { - name: "alternative_shell_invocation", - pattern: r"(/bin/|/usr/bin/|\./)?(bash|sh|zsh|fish|csh|tcsh|dash)\s+-c\s+.*[;&|]", - description: "Alternative shell invocation patterns", - risk_level: RiskLevel::Medium, - category: ThreatCategory::CommandInjection, - }, - // Additional dangerous commands that might be missing ThreatPattern { name: "docker_privileged_exec", pattern: r"docker\s+(run|exec).*--privileged", @@ -280,7 +272,7 @@ pub const THREAT_PATTERNS: &[ThreatPattern] = &[ }, ThreatPattern { name: "log_manipulation", - pattern: r"(>\s*/dev/null|truncate.*log|rm.*\.log|echo\s*>\s*/var/log)", + pattern: r"(truncate.*log|rm\s+((--[a-zA-Z][a-zA-Z\-]*|--|-[a-zA-Z]+)\s+)*/var/log(/|\s|[;&|]|$)|echo\s*>\s*/var/log)", description: "Log file manipulation or deletion", risk_level: RiskLevel::Medium, category: ThreatCategory::SystemModification, @@ -418,18 +410,80 @@ mod tests { } #[test] - fn rm_rf_system_matches_absolute_and_relative() { - let pat = "rm_rf_system"; - assert!(matches(pat, "rm -rf /etc")); - assert!(matches(pat, "rm -rf /usr/bin")); - assert!(matches(pat, "rm -rf etc")); - assert!(matches(pat, "rm -rf var")); + fn rm_rf_home_or_root_matches_bare_targets() { + let pat = "rm_rf_home_or_root"; + assert!(matches(pat, "rm -rf ~")); + assert!(matches(pat, "rm -rf ~/")); + assert!(matches(pat, "rm -rf $HOME")); + assert!(matches(pat, "rm -rf $HOME/")); + assert!(matches(pat, "rm -rf ${HOME}")); + assert!(matches(pat, r#"rm -rf "${HOME}""#)); + assert!(matches(pat, "rm -rf /home")); + assert!(matches(pat, "rm -rf /home/")); + assert!(matches(pat, "rm -rf /root")); + assert!(matches(pat, "rm -rf /root/")); + assert!(matches(pat, "rm -fr ~")); + assert!(matches(pat, "rm --recursive --force ~")); + assert!(matches(pat, r#"rm -rf "$HOME""#)); + assert!(matches(pat, "rm -rf ~; echo done")); + // Wildcard wipes of contents + assert!(matches(pat, "rm -rf /home/*")); + assert!(matches(pat, "rm -rf /root/*")); + assert!(matches(pat, "rm -rf ~/*")); + assert!(matches(pat, "rm -rf ${HOME}/*")); + assert!(matches(pat, r#"rm -rf "/home/*""#)); + // Extra flags and option separator + assert!(matches(pat, "rm -rfv ~")); + assert!(matches(pat, "rm -rf -- ~")); + assert!(matches(pat, "rm --recursive --force -- /home/*")); } #[test] - fn rm_rf_system_no_false_positives() { - let pat = "rm_rf_system"; - assert!(!matches(pat, "rm -rf ./etc-backup")); + fn rm_rf_home_or_root_no_false_positives_on_subdirs() { + let pat = "rm_rf_home_or_root"; + assert!(!matches(pat, "rm -rf ~/Documents/my-gh-repo")); + assert!(!matches(pat, "rm -rf ~/.cache")); + assert!(!matches(pat, "rm -rf $HOME/build")); + assert!(!matches(pat, "rm -rf ${HOME}/build")); + assert!(!matches(pat, "rm -rf ${HOMEDIR}")); + assert!(!matches(pat, "rm -rf /home/user")); assert!(!matches(pat, "rm -rf /home/user/project")); + assert!(!matches(pat, "rm -rf /root/tmp")); + assert!(!matches(pat, "rm -rf ./home")); + assert!(!matches(pat, "rm -rf $HOMEDIR")); + // Wildcards inside subdirs should not match + assert!(!matches(pat, "rm -rf /home/user/*")); + assert!(!matches(pat, "rm -rf ~/Documents/*")); + // Flags that cannot recursively remove directories should not fire + assert!(!matches(pat, "rm -i /root")); + assert!(!matches(pat, "rm -f ~")); + assert!(!matches(pat, "rm --force ~")); + assert!(!matches(pat, "rm --help ~")); + assert!(!matches(pat, "rm -v ~")); + assert!(!matches(pat, "rm -- ~")); + assert!(!matches(pat, "rm ~")); + } + + #[test] + fn log_manipulation_no_dev_null_false_positives() { + let pat = "log_manipulation"; + // Standard stderr suppression should NOT match + assert!(!matches(pat, "ls 2>/dev/null")); + assert!(!matches(pat, "rm -f /tmp/file 2>/dev/null")); + assert!(!matches(pat, "command > /dev/null 2>&1")); + // Actual log tampering should still match + assert!(matches(pat, "truncate -s 0 /var/log/auth.log")); + assert!(matches(pat, "echo > /var/log/syslog")); + assert!(matches(pat, "rm -f /var/log/auth.log")); + assert!(matches(pat, "rm -rf /var/log/syslog")); + assert!(matches(pat, "rm -fr /var/log/auth.log")); + assert!(matches(pat, "rm -rf /var/log")); + assert!(matches(pat, "rm --recursive --force /var/log/auth.log")); + assert!(matches(pat, "rm --recursive /var/log")); + assert!(matches(pat, "rm -rf -- /var/log/auth.log")); + assert!(matches(pat, "rm -rfv /var/log/auth.log")); + // Similar-looking paths outside /var/log should NOT match + assert!(!matches(pat, "rm -rf /var/log-backup")); + assert!(!matches(pat, "rm -rf /var/logs")); } }