From ffe6f9917f9a237ec4c8a70e37e17cce9d8e0c86 Mon Sep 17 00:00:00 2001 From: Chris Bookholt Date: Mon, 22 Jun 2026 11:05:55 -0700 Subject: [PATCH] Require approval for path-qualified safe commands --- codex-rs/core/src/exec_policy_tests.rs | 21 +++++++++++ .../src/command_safety/is_safe_command.rs | 35 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/codex-rs/core/src/exec_policy_tests.rs b/codex-rs/core/src/exec_policy_tests.rs index b6b6bcd23d..cab401e0ca 100644 --- a/codex-rs/core/src/exec_policy_tests.rs +++ b/codex-rs/core/src/exec_policy_tests.rs @@ -1178,6 +1178,27 @@ fn git_branch_requires_approval_for_untrusted_projects() { ); } +#[test] +fn path_qualified_safe_basename_requires_approval_for_untrusted_projects() { + let executable = if cfg!(windows) { r".\cat.exe" } else { "./cat" }; + let command = vec![executable.to_string(), "Cargo.toml".to_string()]; + + assert_eq!( + Decision::Prompt, + render_decision_for_unmatched_command( + &command, + UnmatchedCommandContext { + approval_policy: AskForApproval::UnlessTrusted, + permission_profile: &PermissionProfile::workspace_write(), + windows_sandbox_level: WindowsSandboxLevel::Disabled, + sandbox_permissions: SandboxPermissions::UseDefault, + used_complex_parsing: false, + command_origin: ExecPolicyCommandOrigin::Generic, + }, + ) + ); +} + #[test] fn managed_cwd_write_profile_has_filesystem_restrictions() { let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![ diff --git a/codex-rs/shell-command/src/command_safety/is_safe_command.rs b/codex-rs/shell-command/src/command_safety/is_safe_command.rs index d4431e9a47..6e11d8150a 100644 --- a/codex-rs/shell-command/src/command_safety/is_safe_command.rs +++ b/codex-rs/shell-command/src/command_safety/is_safe_command.rs @@ -64,6 +64,12 @@ fn is_safe_to_call_with_exec(command: &[String]) -> bool { let Some(cmd0) = command.first().map(String::as_str) else { return false; }; + if std::path::Path::new(cmd0).components().count() != 1 { + // A workspace executable can impersonate an allowlisted utility by + // reusing its basename. Only bare names resolved through the trusted + // process PATH are eligible for generic safe-command classification. + return false; + } match executable_name_lookup_key(cmd0).as_deref() { Some(cmd) if cfg!(target_os = "linux") && matches!(cmd, "numfmt" | "tac") => true, @@ -250,6 +256,35 @@ mod tests { } } + #[test] + fn path_qualified_safe_command_names_require_approval() { + let absolute_cat = if cfg!(windows) { + r"C:\workspace\cat.exe" + } else { + "/tmp/workspace/cat" + }; + let parent_relative_cat = if cfg!(windows) { + r"..\cat.exe" + } else { + "../cat" + }; + + for args in [ + vec_str(&["./cat", "Cargo.toml"]), + vec_str(&[parent_relative_cat, "Cargo.toml"]), + vec_str(&[absolute_cat, "Cargo.toml"]), + vec_str(&["bash", "-lc", "./cat Cargo.toml"]), + ] { + assert!( + !is_known_safe_command(&args), + "expected path-qualified executable {args:?} to require approval", + ); + } + + let bare_cat = if cfg!(windows) { "cat.exe" } else { "cat" }; + assert!(is_known_safe_command(&vec_str(&[bare_cat, "Cargo.toml"]))); + } + #[test] fn git_commands_require_approval() { for args in [