mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
Harden PowerShell exec-policy classification
This commit is contained in:
@@ -5,6 +5,7 @@ pub mod is_safe_command;
|
||||
#[cfg(windows)]
|
||||
pub(crate) mod windows_safe_commands;
|
||||
pub(crate) use powershell_parser::TrustedPowerShellFlavor;
|
||||
pub(crate) use powershell_parser::is_trusted_powershell_parser_executable;
|
||||
#[cfg(all(test, windows))]
|
||||
pub(crate) use powershell_parser::trusted_standard_pwsh_invocation_path;
|
||||
#[cfg(all(test, windows))]
|
||||
|
||||
@@ -120,6 +120,10 @@ fn trusted_powershell_parser_executable(executable: &str) -> Option<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_trusted_powershell_parser_executable(executable: &str) -> bool {
|
||||
trusted_powershell_parser_executable(executable).is_some()
|
||||
}
|
||||
|
||||
/// Returns the authoritative Windows PowerShell invocation path only when the production trust
|
||||
/// resolver accepts that exact known-folder-derived path.
|
||||
#[cfg(all(test, windows))]
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
use crate::command_safety::TrustedPowerShellFlavor;
|
||||
use crate::command_safety::is_trusted_powershell_parser_executable;
|
||||
use crate::command_safety::try_parse_powershell_ast_commands;
|
||||
use crate::command_safety::try_parse_powershell_ast_commands_with_trusted_flavor;
|
||||
use crate::shell_detect::ShellType;
|
||||
use crate::shell_detect::detect_shell_type;
|
||||
|
||||
const POWERSHELL_FLAGS: &[&str] = &["-nologo", "-noprofile", "-command", "-c"];
|
||||
|
||||
@@ -48,12 +45,7 @@ pub fn extract_powershell_command(command: &[String]) -> Option<(&str, &str)> {
|
||||
}
|
||||
|
||||
let shell = &command[0];
|
||||
if !matches!(
|
||||
detect_shell_type(PathBuf::from(shell)),
|
||||
Some(ShellType::PowerShell)
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
powershell_flavor_for_executable(shell)?;
|
||||
|
||||
// Find the first occurrence of -Command (accept common short alias -c as well)
|
||||
let mut i = 1usize;
|
||||
@@ -64,6 +56,9 @@ pub fn extract_powershell_command(command: &[String]) -> Option<(&str, &str)> {
|
||||
return None;
|
||||
}
|
||||
if flag.eq_ignore_ascii_case("-Command") || flag.eq_ignore_ascii_case("-c") {
|
||||
if i + 2 != command.len() {
|
||||
return None;
|
||||
}
|
||||
let script = &command[i + 1];
|
||||
return Some((shell, script));
|
||||
}
|
||||
@@ -91,6 +86,51 @@ pub enum PowerShellFlavor {
|
||||
PowerShell7,
|
||||
}
|
||||
|
||||
/// Result of inspecting a PowerShell wrapper for exec-policy evaluation.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum PowerShellExecPolicyParse {
|
||||
/// The runtime wrapper is the same protected executable used for parsing.
|
||||
TrustedRuntime { commands: Option<Vec<Vec<String>>> },
|
||||
/// A protected parser inspected the body, but the runtime wrapper remains untrusted.
|
||||
UntrustedRuntime { commands: Option<Vec<Vec<String>>> },
|
||||
}
|
||||
|
||||
/// Inspects a PowerShell wrapper without spawning its model-selected runtime.
|
||||
pub fn parse_powershell_command_for_exec_policy(
|
||||
command: &[String],
|
||||
) -> Option<PowerShellExecPolicyParse> {
|
||||
let executable = command.first()?;
|
||||
let flavor = powershell_flavor_for_executable(executable)?;
|
||||
let commands = extract_powershell_command(command)
|
||||
.and_then(|(_, script)| parse_powershell_script_with_trusted_parser(flavor, script));
|
||||
if is_trusted_powershell_parser_executable(executable) {
|
||||
Some(PowerShellExecPolicyParse::TrustedRuntime { commands })
|
||||
} else {
|
||||
Some(PowerShellExecPolicyParse::UntrustedRuntime { commands })
|
||||
}
|
||||
}
|
||||
|
||||
fn powershell_flavor_for_executable(executable: &str) -> Option<PowerShellFlavor> {
|
||||
let mut executable_name = std::path::Path::new(executable).file_name()?.to_str()?;
|
||||
#[cfg(windows)]
|
||||
{
|
||||
executable_name = executable_name.trim_end_matches([' ', '.']);
|
||||
}
|
||||
loop {
|
||||
let executable_stem = std::path::Path::new(executable_name)
|
||||
.file_stem()?
|
||||
.to_str()?;
|
||||
if executable_stem.eq_ignore_ascii_case("powershell") {
|
||||
return Some(PowerShellFlavor::WindowsPowerShell);
|
||||
} else if executable_stem.eq_ignore_ascii_case("pwsh") {
|
||||
return Some(PowerShellFlavor::PowerShell7);
|
||||
} else if executable_stem == executable_name {
|
||||
return None;
|
||||
}
|
||||
executable_name = executable_stem;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a PowerShell script without using the runtime wrapper as the parser executable.
|
||||
///
|
||||
/// The selected parser comes from the authoritative Windows System or Program Files known folder.
|
||||
@@ -176,14 +216,20 @@ fn is_powershellish_executable_available(powershell_or_pwsh_exe: &std::path::Pat
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(windows)]
|
||||
use super::PowerShellExecPolicyParse;
|
||||
#[cfg(windows)]
|
||||
use super::PowerShellFlavor;
|
||||
use super::UTF8_OUTPUT_PREFIX;
|
||||
use super::extract_powershell_command;
|
||||
#[cfg(windows)]
|
||||
use super::parse_powershell_command_for_exec_policy;
|
||||
#[cfg(windows)]
|
||||
use super::parse_powershell_command_into_plain_commands;
|
||||
#[cfg(windows)]
|
||||
use super::parse_powershell_script_with_trusted_parser;
|
||||
#[cfg(windows)]
|
||||
use super::powershell_flavor_for_executable;
|
||||
use super::prefix_powershell_script_with_utf8;
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -241,6 +287,18 @@ mod tests {
|
||||
assert_eq!(script, "Get-ChildItem | Select-String foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_arguments_after_powershell_script() {
|
||||
let command = vec![
|
||||
"powershell.exe".to_string(),
|
||||
"-Command".to_string(),
|
||||
"Write-Host hi".to_string(),
|
||||
"unexpected".to_string(),
|
||||
];
|
||||
|
||||
assert_eq!(extract_powershell_command(&command), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefixes_powershell_command_with_best_effort_utf8() {
|
||||
let cmd = vec![
|
||||
@@ -330,4 +388,103 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn exec_policy_parse_preserves_runtime_trust_and_aliases() {
|
||||
assert_eq!(
|
||||
powershell_flavor_for_executable(r"\\.\UNC\server\share\pwsh.EXE.bAt "),
|
||||
Some(PowerShellFlavor::PowerShell7)
|
||||
);
|
||||
let trusted = trusted_windows_powershell_executable();
|
||||
let parsed = Some(vec![vec!["echo".to_string(), "classified".to_string()]]);
|
||||
let cases = [
|
||||
(trusted, true),
|
||||
("powershell.exe".to_string(), false),
|
||||
("powershell.EXE.CmD".to_string(), false),
|
||||
(r".\tools\powershell.exe".to_string(), false),
|
||||
(r"C:\workspace\powershell.exe".to_string(), false),
|
||||
("powershell.exe.".to_string(), false),
|
||||
("powershell.exe ".to_string(), false),
|
||||
(r"\\?\C:\workspace\powershell.exe.".to_string(), false),
|
||||
(r"\\.\C:\workspace\powershell.exe ".to_string(), false),
|
||||
(r"\\.\UNC\server\share\powershell.exe ".to_string(), false),
|
||||
];
|
||||
|
||||
for (executable, trusted) in cases {
|
||||
let result = parse_powershell_command_for_exec_policy(&[
|
||||
executable.clone(),
|
||||
"-NoProfile".to_string(),
|
||||
"-Command".to_string(),
|
||||
"echo classified".to_string(),
|
||||
]);
|
||||
let expected = if trusted {
|
||||
PowerShellExecPolicyParse::TrustedRuntime {
|
||||
commands: parsed.clone(),
|
||||
}
|
||||
} else {
|
||||
PowerShellExecPolicyParse::UntrustedRuntime {
|
||||
commands: parsed.clone(),
|
||||
}
|
||||
};
|
||||
assert_eq!(result, Some(expected), "runtime {executable:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn ordinary_trailing_aliases_launch_but_remain_untrusted() {
|
||||
let trusted = trusted_windows_powershell_executable();
|
||||
for executable in [format!("{trusted}."), format!("{trusted} ")] {
|
||||
assert!(super::is_powershellish_executable_available(
|
||||
std::path::Path::new(&executable)
|
||||
));
|
||||
assert!(matches!(
|
||||
parse_powershell_command_for_exec_policy(&[
|
||||
executable,
|
||||
"-Command".to_string(),
|
||||
"echo launchable".to_string(),
|
||||
]),
|
||||
Some(PowerShellExecPolicyParse::UntrustedRuntime { commands: Some(_) })
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn exec_policy_parse_marks_unsupported_bodies_opaque() {
|
||||
let command = |executable: &str, args: &[&str]| {
|
||||
std::iter::once(executable.to_string())
|
||||
.chain(args.iter().map(|arg| (*arg).to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let cases = [
|
||||
(
|
||||
command(
|
||||
&trusted_windows_powershell_executable(),
|
||||
&["-Command", "param([string]$path) echo blocked"],
|
||||
),
|
||||
PowerShellExecPolicyParse::TrustedRuntime { commands: None },
|
||||
),
|
||||
(
|
||||
command(
|
||||
"powershell.exe",
|
||||
&["-NonInteractive", "-Command", "echo blocked"],
|
||||
),
|
||||
PowerShellExecPolicyParse::UntrustedRuntime { commands: None },
|
||||
),
|
||||
(
|
||||
command("powershell.exe", &["-Command", "echo blocked", "trailing"]),
|
||||
PowerShellExecPolicyParse::UntrustedRuntime { commands: None },
|
||||
),
|
||||
];
|
||||
|
||||
for (command, expected) in cases {
|
||||
assert_eq!(
|
||||
parse_powershell_command_for_exec_policy(&command),
|
||||
Some(expected),
|
||||
"command {command:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user