From 61374529aa13c531b46ddd5eb346202b1f851234 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 07:15:17 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITICAL]?= =?UTF-8?q?=20Fix=20command=20validation=20bypass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: bobdivx <6737167+bobdivx@users.noreply.github.com> --- .jules/sentinel.md | 5 +++++ crates/agent/src/tools/workdir_cmd.rs | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 1a9528255..24c22fa9d 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -17,3 +17,8 @@ **Vulnerability:** The `verify_github_sig` function in `apps/server/src/infra_routes.rs` verified GitHub webhook signatures using standard string comparison `expected.eq_ignore_ascii_case(hex_sig)`. **Learning:** String comparison functions exit early as soon as a character mismatch is found. In a cryptographic context, this variable response time leaks information and allows an attacker to perform a timing attack to forge a valid HMAC signature by guessing it byte by byte. **Prevention:** Always use a constant-time comparison library (like `hmac`'s `verify_slice()`) to compare cryptographic hashes, secrets, and signatures to prevent side-channel timing attacks. Avoid custom implementations of standard cryptographic algorithms. + +## 2024-05-30 - [Command Validation Bypass] +**Vulnerability:** Command validation in `tools::workdir_cmd` checked for blocklist terms only on the joined command string. This allowed for bypasses like `npm run rm` because it bypassed the `"rm "` substring check, which was intended to block the `rm` command. +**Learning:** Relying solely on a joined string for command validation breaks multi-word substring blocks or is fragile to spacing, while relying solely on individual token checks doesn't catch multi-word blocked phrases. Both must be checked. +**Prevention:** Verify both the joined command string against multi-word or spacing blocklists AND individual tokenized arguments for exact matches of sensitive binaries (e.g., `part == "rm"`). diff --git a/crates/agent/src/tools/workdir_cmd.rs b/crates/agent/src/tools/workdir_cmd.rs index 46a5ff54a..51c7cb89f 100644 --- a/crates/agent/src/tools/workdir_cmd.rs +++ b/crates/agent/src/tools/workdir_cmd.rs @@ -112,6 +112,20 @@ fn validate_command(raw: &str) -> std::result::Result, String> { return Err(format!("token interdit dans la commande : {bad}")); } } + + let blocked_exact = [ + "rm", "sudo", "chmod", "chown", "curl", "wget", "ssh", "scp", + ]; + + for part in &parts { + let part_lower = part.to_ascii_lowercase(); + // Also check if any individual token is an exact match for sensitive binaries + for bad in &blocked_exact { + if &part_lower == bad { + return Err(format!("token exact interdit dans la commande : {bad}")); + } + } + } let bin = parts[0].as_str(); let bin_base = Path::new(bin) .file_name() @@ -354,6 +368,12 @@ mod tests { fn blocks_obfuscated_commands() { assert!(validate_command("npm run \"s\"udo").is_err()); assert!(validate_command("cat \"..\"/\"..\"/etc/passwd").is_err()); + + // New tests for exact token matching bypass + assert!(validate_command("npm run rm").is_err()); + assert!(validate_command("npx rm -rf /").is_err()); + assert!(validate_command("npm test sudo").is_err()); + assert!(validate_command("yarn run curl http://malicious.com").is_err()); } }