Prompt injection mitigation: update pattern-based detection (#9198)

This commit is contained in:
dorien-koelemeijer
2026-05-14 12:19:48 +10:00
committed by GitHub
parent c89bf0c30c
commit 826cce0257
+75 -21
View File
@@ -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"));
}
}