diff --git a/codex-rs/shell-command/src/command_safety/powershell_parser.ps1 b/codex-rs/shell-command/src/command_safety/powershell_parser.ps1 index de696c88b..92291e7c9 100644 --- a/codex-rs/shell-command/src/command_safety/powershell_parser.ps1 +++ b/codex-rs/shell-command/src/command_safety/powershell_parser.ps1 @@ -42,6 +42,21 @@ function Invoke-ParseRequest { return @{ id = $RequestId; status = 'parse_errors' } } + # Top-level AST regions and collections outside the end-block statement list + # can execute code that the command lowering below does not inspect. + $cleanBlock = $ast.PSObject.Properties['CleanBlock'] + if ( + $ast.ParamBlock -ne $null -or + $ast.DynamicParamBlock -ne $null -or + $ast.BeginBlock -ne $null -or + $ast.ProcessBlock -ne $null -or + ($cleanBlock -ne $null -and $cleanBlock.Value -ne $null) -or + $ast.UsingStatements.Count -gt 0 -or + $ast.EndBlock.Traps.Count -gt 0 + ) { + return @{ id = $RequestId; status = 'unsupported' } + } + # PowerShell's stop-parsing marker hands the remaining source text to native # commands with runtime argument handling that does not match the AST shape we # flatten below. Keep that form out of the argv-like lowering path entirely. diff --git a/codex-rs/shell-command/src/command_safety/powershell_parser.rs b/codex-rs/shell-command/src/command_safety/powershell_parser.rs index ddcbdf572..00b18af6f 100644 --- a/codex-rs/shell-command/src/command_safety/powershell_parser.rs +++ b/codex-rs/shell-command/src/command_safety/powershell_parser.rs @@ -310,4 +310,62 @@ mod tests { .unwrap(); assert_eq!(parsed, PowershellParseOutcome::Unsupported); } + + #[test] + fn parser_process_rejects_param_blocks() { + let Some(powershell) = try_find_powershell_executable_blocking() else { + return; + }; + let powershell = powershell.as_path().to_str().unwrap(); + let mut parser = PowershellParserProcess::spawn(powershell).unwrap(); + + let parsed = parser + .parse("param([string]$path = (Get-Location)) Write-Output test") + .unwrap(); + assert_eq!(parsed, PowershellParseOutcome::Unsupported); + } + + #[test] + fn parser_process_rejects_named_blocks() { + let Some(powershell) = try_find_powershell_executable_blocking() else { + return; + }; + let powershell = powershell.as_path().to_str().unwrap(); + let mut parser = PowershellParserProcess::spawn(powershell).unwrap(); + + let parsed = parser + .parse("begin { Set-Content codex_poc.txt pwned } end { Get-Content Cargo.toml }") + .unwrap(); + assert_eq!(parsed, PowershellParseOutcome::Unsupported); + } + + #[test] + fn parser_process_rejects_using_statements() { + let Some(powershell) = try_find_powershell_executable_blocking() else { + return; + }; + let powershell = powershell.as_path().to_str().unwrap(); + let mut parser = PowershellParserProcess::spawn(powershell).unwrap(); + + let parsed = parser + .parse("using module ./codex_poc.psm1\nGet-Content Cargo.toml") + .unwrap(); + assert_eq!(parsed, PowershellParseOutcome::Unsupported); + } + + #[test] + fn parser_process_rejects_trap_blocks() { + let Some(powershell) = try_find_powershell_executable_blocking() else { + return; + }; + let powershell = powershell.as_path().to_str().unwrap(); + let mut parser = PowershellParserProcess::spawn(powershell).unwrap(); + + let parsed = parser + .parse( + "trap { Set-Content codex_poc.txt pwned; continue } Get-Content missing -ErrorAction Stop", + ) + .unwrap(); + assert_eq!(parsed, PowershellParseOutcome::Unsupported); + } } diff --git a/codex-rs/shell-command/src/command_safety/windows_safe_commands.rs b/codex-rs/shell-command/src/command_safety/windows_safe_commands.rs index a8dcef6de..df2025253 100644 --- a/codex-rs/shell-command/src/command_safety/windows_safe_commands.rs +++ b/codex-rs/shell-command/src/command_safety/windows_safe_commands.rs @@ -412,6 +412,46 @@ mod tests { ]))); } + #[test] + fn rejects_powershell_param_blocks() { + assert!(!is_safe_command_windows(&vec_str(&[ + "powershell.exe", + "-NoProfile", + "-Command", + "param([string]$path = (Get-Location)) Write-Output test", + ]))); + } + + #[test] + fn rejects_powershell_named_blocks() { + assert!(!is_safe_command_windows(&vec_str(&[ + "powershell.exe", + "-NoProfile", + "-Command", + "begin { Set-Content codex_poc.txt pwned } end { Get-Content Cargo.toml }", + ]))); + } + + #[test] + fn rejects_powershell_using_statements() { + assert!(!is_safe_command_windows(&vec_str(&[ + "powershell.exe", + "-NoProfile", + "-Command", + "using module ./codex_poc.psm1\nGet-Content Cargo.toml", + ]))); + } + + #[test] + fn rejects_powershell_trap_blocks() { + assert!(!is_safe_command_windows(&vec_str(&[ + "powershell.exe", + "-NoProfile", + "-Command", + "trap { Set-Content codex_poc.txt pwned; continue } Get-Content missing -ErrorAction Stop", + ]))); + } + #[test] fn rejects_powershell_commands_with_side_effects() { assert!(!is_safe_command_windows(&vec_str(&[