fix(platform): 收紧 safety guard 的 format 正则并按平台分组
原 \bformat\s+ 正则精度不足,会误伤 dart format、buf format、 pytest --format 等合法命令;Remove-Item 大写正则经 to_lowercase 后永远匹配不到,是潜在 bug。 改动: - format 正则收紧为 \bformat\s+.*[a-z]:,要求出现盘符才拦截 - 按 Platform 分组注入规则,Unix 不再注入 Windows 专用规则 - Remove-Item 正则改为小写,与 guard_command 的大小写处理一致 - 平台相关测试加 #[cfg] 属性,避免跨平台失败 - 新增 test_format_pattern_precision 覆盖危险命令与误伤场景 - 新增 test_legitimate_format_commands_not_blocked 回归测试 验证: - cargo test --lib platform:: 11/11 通过 - cargo test --lib tools::bash 13/13 通过
This commit is contained in:
parent
c7ee6bb519
commit
f37a5ffe6e
@ -93,22 +93,47 @@ impl ShellInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Dangerous command patterns for safety guards.
|
/// Dangerous command patterns for safety guards.
|
||||||
|
///
|
||||||
|
/// Returns patterns filtered by the current platform. Platform-specific
|
||||||
|
/// rules (e.g. `format` on Windows, `rm` on Unix) are only injected on
|
||||||
|
/// their target platform to avoid false positives.
|
||||||
pub fn dangerous_command_patterns() -> Vec<String> {
|
pub fn dangerous_command_patterns() -> Vec<String> {
|
||||||
vec![
|
dangerous_command_patterns_for_platform(Platform::current())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Platform-specific dangerous command patterns.
|
||||||
|
///
|
||||||
|
/// Exposed primarily for testing. Callers should prefer
|
||||||
|
/// [`dangerous_command_patterns`] which auto-detects the platform.
|
||||||
|
pub fn dangerous_command_patterns_for_platform(platform: Platform) -> Vec<String> {
|
||||||
|
let mut patterns: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
// Cross-platform: fork bomb
|
||||||
|
patterns.push(r":\(\)\s*\{.*\};\s*:".to_string());
|
||||||
|
|
||||||
|
match platform {
|
||||||
|
Platform::Unix => {
|
||||||
// Unix dangerous commands
|
// Unix dangerous commands
|
||||||
r"\brm\s+-[rf]{1,2}\b".to_string(),
|
patterns.push(r"\brm\s+-[rf]{1,2}\b".to_string());
|
||||||
r"\bchmod\s+-[Rr]".to_string(),
|
patterns.push(r"\bchmod\s+-[Rr]".to_string());
|
||||||
r"\bchown\s+-[Rr]".to_string(),
|
patterns.push(r"\bchown\s+-[Rr]".to_string());
|
||||||
// Windows dangerous commands
|
}
|
||||||
r"\bdel\s+/[fq]\b".to_string(),
|
Platform::Windows => {
|
||||||
r"\brmdir\s+/s\b".to_string(),
|
// Windows cmd dangerous commands.
|
||||||
r"\bformat\s+".to_string(),
|
// `format` requires a drive letter (`[a-z]:`) somewhere after it,
|
||||||
// PowerShell dangerous commands
|
// so legitimate uses like `dart format lib/` or
|
||||||
r"\bRemove-Item\s+.*-Recurse".to_string(),
|
// `pytest --format json` (no drive letter) are not matched.
|
||||||
r"\bRemove-Item\s+.*-Force".to_string(),
|
patterns.push(r"\bformat\s+.*[a-z]:".to_string());
|
||||||
// Fork bomb (cross-platform)
|
patterns.push(r"\bdel\s+/[fq]\b".to_string());
|
||||||
r":\(\)\s*\{.*\};\s*:".to_string(),
|
patterns.push(r"\brmdir\s+/s\b".to_string());
|
||||||
]
|
// PowerShell dangerous commands. Patterns are lowercase because
|
||||||
|
// `guard_command` lowercases the command string before matching.
|
||||||
|
patterns.push(r"\bremove-item\s+.*-recurse".to_string());
|
||||||
|
patterns.push(r"\bremove-item\s+.*-force".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
patterns
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check whether a child process is blocked waiting for stdin input.
|
/// Check whether a child process is blocked waiting for stdin input.
|
||||||
@ -435,9 +460,64 @@ mod tests {
|
|||||||
fn test_dangerous_patterns() {
|
fn test_dangerous_patterns() {
|
||||||
let patterns = dangerous_command_patterns();
|
let patterns = dangerous_command_patterns();
|
||||||
assert!(!patterns.is_empty());
|
assert!(!patterns.is_empty());
|
||||||
// Should contain patterns for both platforms
|
// Cross-platform fork bomb rule is always present
|
||||||
|
assert!(patterns.iter().any(|p| p.contains(r":\(\)")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dangerous_patterns_unix() {
|
||||||
|
let patterns = dangerous_command_patterns_for_platform(Platform::Unix);
|
||||||
|
// Unix-specific rules
|
||||||
assert!(patterns.iter().any(|p| p.contains("rm")));
|
assert!(patterns.iter().any(|p| p.contains("rm")));
|
||||||
|
assert!(patterns.iter().any(|p| p.contains("chmod")));
|
||||||
|
assert!(patterns.iter().any(|p| p.contains("chown")));
|
||||||
|
// Windows-specific rules must NOT be present on Unix
|
||||||
|
assert!(!patterns.iter().any(|p| p.contains("format")));
|
||||||
|
assert!(!patterns.iter().any(|p| p.contains("del")));
|
||||||
|
assert!(!patterns.iter().any(|p| p.contains("remove-item")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dangerous_patterns_windows() {
|
||||||
|
let patterns = dangerous_command_patterns_for_platform(Platform::Windows);
|
||||||
|
// Windows-specific rules
|
||||||
assert!(patterns.iter().any(|p| p.contains("del")));
|
assert!(patterns.iter().any(|p| p.contains("del")));
|
||||||
|
assert!(patterns.iter().any(|p| p.contains("format")));
|
||||||
|
assert!(patterns.iter().any(|p| p.contains("remove-item")));
|
||||||
|
// Unix-specific rules must NOT be present on Windows.
|
||||||
|
// Use r"\brm\s" to match rm-as-command without matching rmdir.
|
||||||
|
assert!(!patterns.iter().any(|p| p.contains(r"\brm\s")));
|
||||||
|
assert!(!patterns.iter().any(|p| p.contains("chmod")));
|
||||||
|
assert!(!patterns.iter().any(|p| p.contains("chown")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_pattern_precision() {
|
||||||
|
let patterns = dangerous_command_patterns_for_platform(Platform::Windows);
|
||||||
|
let format_pat = patterns
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.contains("format"))
|
||||||
|
.expect("format pattern should exist on Windows");
|
||||||
|
let re = regex::Regex::new(format_pat).unwrap();
|
||||||
|
|
||||||
|
// Helper: guard_command lowercases before matching, so tests must too.
|
||||||
|
let m = |cmd: &str| re.is_match(&cmd.to_lowercase());
|
||||||
|
|
||||||
|
// Truly dangerous commands — should match
|
||||||
|
assert!(m("format c:"));
|
||||||
|
assert!(m("format /q c:"));
|
||||||
|
assert!(m("format d: /fs:ntfs"));
|
||||||
|
assert!(m("echo ok | format c:"));
|
||||||
|
assert!(m("echo ok; format c:"));
|
||||||
|
// Sub-shell invocation must also be caught
|
||||||
|
assert!(m(r#"cmd /c "format C:""#));
|
||||||
|
|
||||||
|
// Legitimate commands containing literal "format " — should NOT match
|
||||||
|
assert!(!m("dart format lib/"));
|
||||||
|
assert!(!m("buf format -w"));
|
||||||
|
assert!(!m("pytest --format json"));
|
||||||
|
assert!(!m(r#"echo "please format the disk""#));
|
||||||
|
assert!(!m(r#"git log --pretty=format:"%h""#));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -750,6 +750,7 @@ mod tests {
|
|||||||
assert!(result.success);
|
assert!(result.success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_dangerous_rm() {
|
async fn test_dangerous_rm() {
|
||||||
let tool = BashTool::default();
|
let tool = BashTool::default();
|
||||||
@ -763,6 +764,7 @@ mod tests {
|
|||||||
assert!(result.error.unwrap().contains("blocked"));
|
assert!(result.error.unwrap().contains("blocked"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_dangerous_windows_commands() {
|
async fn test_dangerous_windows_commands() {
|
||||||
let tool = BashTool::default();
|
let tool = BashTool::default();
|
||||||
@ -788,6 +790,28 @@ mod tests {
|
|||||||
assert!(result.error.unwrap().contains("blocked"));
|
assert!(result.error.unwrap().contains("blocked"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_legitimate_format_commands_not_blocked() {
|
||||||
|
// Commands that merely contain the literal "format " but are NOT
|
||||||
|
// Windows disk-format operations must not be blocked by safety guard.
|
||||||
|
let tool = BashTool::default();
|
||||||
|
let commands = [
|
||||||
|
r#"echo "please format the disk""#,
|
||||||
|
r#"git log --pretty=format:"%h""#,
|
||||||
|
];
|
||||||
|
for cmd in commands {
|
||||||
|
let result = tool.execute(json!({ "command": cmd })).await.unwrap();
|
||||||
|
if let Some(err) = &result.error {
|
||||||
|
assert!(
|
||||||
|
!err.contains("blocked"),
|
||||||
|
"command {:?} should not be blocked, but got: {}",
|
||||||
|
cmd,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_missing_command() {
|
async fn test_missing_command() {
|
||||||
let tool = BashTool::default();
|
let tool = BashTool::default();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user