Compare commits

...

1 Commits

Author SHA1 Message Date
David Wiesen
7d47a62fc8 Fix Windows execpolicy matching for PowerShell wrappers 2026-04-29 17:05:28 -07:00
4 changed files with 53 additions and 2 deletions

View File

@@ -37,6 +37,7 @@ use crate::sandboxing::SandboxPermissions;
use crate::tools::sandboxing::ExecApprovalRequirement;
use codex_shell_command::bash::parse_shell_lc_plain_commands;
use codex_shell_command::bash::parse_shell_lc_single_command_prefix;
use codex_shell_command::split_powershell_command_sequence;
use codex_utils_absolute_path::AbsolutePathBuf;
use shlex::try_join as shlex_try_join;
@@ -705,6 +706,12 @@ fn commands_for_exec_policy(command: &[String]) -> (Vec<Vec<String>>, bool) {
return (commands, false);
}
if let Some(commands) = split_powershell_command_sequence(command)
&& !commands.is_empty()
{
return (commands, false);
}
if let Some(single_command) = parse_shell_lc_single_command_prefix(command) {
return (vec![single_command], true);
}

View File

@@ -674,6 +674,23 @@ fn commands_for_exec_policy_falls_back_for_whitespace_shell_script() {
assert_eq!(commands_for_exec_policy(&command), (vec![command], false));
}
#[test]
fn commands_for_exec_policy_splits_powershell_command() {
let command = vec![
"pwsh".to_string(),
"-Command".to_string(),
"git add -A".to_string(),
];
assert_eq!(
commands_for_exec_policy(&command),
(
vec![vec!["git".to_string(), "add".to_string(), "-A".to_string(),]],
false,
)
);
}
#[tokio::test]
async fn ignore_user_config_keeps_user_policy_files() -> std::io::Result<()> {
let temp = tempdir()?;
@@ -1637,6 +1654,32 @@ prefix_rule(pattern=["bash"], decision="allow")
.await;
}
#[tokio::test]
async fn powershell_command_bypasses_sandbox_when_inner_command_matches_policy_allow() {
let policy_src = r#"prefix_rule(pattern=["git", "add"], decision="allow")"#;
assert_exec_approval_requirement_for_command(
ExecApprovalRequirementScenario {
policy_src: Some(policy_src.to_string()),
command: vec![
"pwsh".to_string(),
"-Command".to_string(),
"git add -A".to_string(),
],
approval_policy: AskForApproval::OnRequest,
sandbox_policy: SandboxPolicy::new_workspace_write_policy(),
file_system_sandbox_policy: workspace_write_file_system_sandbox_policy(),
sandbox_permissions: SandboxPermissions::RequireEscalated,
prefix_rule: None,
},
ExecApprovalRequirement::Skip {
bypass_sandbox: true,
proposed_execpolicy_amendment: None,
},
)
.await;
}
fn derive_requested_execpolicy_amendment_for_test(
prefix_rule: Option<&Vec<String>>,
matched_rules: &[RuleMatch],

View File

@@ -6,7 +6,7 @@ use std::path::Path;
/// On Windows, we conservatively allow only clearly read-only PowerShell invocations
/// that match a small safelist. Anything else (including direct CMD commands) is unsafe.
pub fn is_safe_command_windows(command: &[String]) -> bool {
if let Some(commands) = try_parse_powershell_command_sequence(command) {
if let Some(commands) = split_powershell_command_sequence(command) {
commands
.iter()
.all(|cmd| is_safe_powershell_command(cmd.as_slice()))
@@ -18,7 +18,7 @@ pub fn is_safe_command_windows(command: &[String]) -> bool {
/// Returns each command sequence if the invocation starts with a PowerShell binary.
/// For example, the tokens from `pwsh Get-ChildItem | Measure-Object` become two sequences.
fn try_parse_powershell_command_sequence(command: &[String]) -> Option<Vec<Vec<String>>> {
pub fn split_powershell_command_sequence(command: &[String]) -> Option<Vec<Vec<String>>> {
let (exe, rest) = command.split_first()?;
if is_powershell_executable(exe) {
parse_powershell_invocation(exe, rest)

View File

@@ -9,3 +9,4 @@ pub mod powershell;
pub use command_safety::is_dangerous_command;
pub use command_safety::is_safe_command;
pub use command_safety::windows_safe_commands::split_powershell_command_sequence;