Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions codex-rs/shell-command/src/command_safety/powershell_parser.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
58 changes: 58 additions & 0 deletions codex-rs/shell-command/src/command_safety/powershell_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
40 changes: 40 additions & 0 deletions codex-rs/shell-command/src/command_safety/windows_safe_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(&[
Expand Down
Loading