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
20 changes: 15 additions & 5 deletions codex-rs/core/src/exec_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ use codex_shell_command::bash::parse_shell_lc_single_command_prefix;
use codex_utils_absolute_path::AbsolutePathBuf;
use shlex::try_join as shlex_try_join;

mod executable_identity;
mod model_policy;

pub(crate) use model_policy::AllowPrefixRules;
Expand Down Expand Up @@ -314,6 +315,20 @@ impl ExecPolicyManager {
pub(crate) async fn create_exec_approval_requirement_for_command(
&self,
req: ExecApprovalRequest<'_>,
) -> ExecApprovalRequirement {
let commands = commands_for_exec_policy(req.command);
self.create_exec_approval_requirement_for_parsed_commands(req, commands)
.await
}

async fn create_exec_approval_requirement_for_parsed_commands(
&self,
req: ExecApprovalRequest<'_>,
ExecPolicyCommands {
commands,
used_complex_parsing,
command_origin,
}: ExecPolicyCommands,
) -> ExecApprovalRequirement {
let ExecApprovalRequest {
command,
Expand All @@ -326,11 +341,6 @@ impl ExecPolicyManager {
allow_prefix_rules,
} = req;
let exec_policy = self.current_for_environment(environment_policy, allow_prefix_rules);
let ExecPolicyCommands {
commands,
used_complex_parsing,
command_origin,
} = commands_for_exec_policy(command);
// Keep heredoc prefix parsing for the rules that apply to this model,
// but avoid reusable approvals for cyber models or when only the
// heredoc fallback parser matched.
Expand Down
106 changes: 106 additions & 0 deletions codex-rs/core/src/exec_policy/executable_identity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use super::ExecApprovalRequest;
#[cfg(windows)]
use super::ExecPolicyCommandOrigin;
#[cfg(windows)]
use super::ExecPolicyCommands;
use super::ExecPolicyManager;
use super::commands_for_exec_policy;
use crate::shell::Shell;
use crate::tools::sandboxing::ExecApprovalRequirement;
#[cfg(windows)]
use codex_shell_command::powershell::extract_powershell_command;
#[cfg(windows)]
use codex_shell_command::powershell::parse_powershell_script_into_plain_commands;
use codex_tools::UnifiedExecShellMode;
use std::path::Path;

impl ExecPolicyManager {
pub(crate) async fn create_exec_approval_requirement_for_shell(
&self,
mut request: ExecApprovalRequest<'_>,
configured_shell: &Shell,
shell_mode: &UnifiedExecShellMode,
) -> ExecApprovalRequirement {
let command = request.command;
let executable = shell_approval_command(command, configured_shell, shell_mode);
if executable.len() == command.len() {
return self
.create_exec_approval_requirement_for_command(request)
.await;
}

#[cfg(windows)]
let mut policy_commands = match extract_powershell_command(command) {
Some((_, script)) => ExecPolicyCommands {
commands: parse_powershell_script_into_plain_commands(script)
.unwrap_or_else(|| vec![command.to_vec()]),
used_complex_parsing: false,
command_origin: ExecPolicyCommandOrigin::PowerShell,
},
None => commands_for_exec_policy(command),
};

#[cfg(not(windows))]
let mut policy_commands = commands_for_exec_policy(command);

// Evaluate the executable alongside its apparent commands. Inner
// commands can add restrictions, but cannot grant the executable trust.
policy_commands.commands.insert(0, executable.to_vec());
request.command = executable;
self.create_exec_approval_requirement_for_parsed_commands(request, policy_commands)
.await
}
}

fn shell_approval_command<'a>(
command: &'a [String],
configured_shell: &Shell,
shell_mode: &UnifiedExecShellMode,
) -> &'a [String] {
let Some(executable) = command.first() else {
return command;
};
let executable_path = Path::new(executable);

#[cfg(windows)]
let is_system_shell = std::env::var_os("SystemRoot").is_some_and(|system_root| {
let system_directory = Path::new(&system_root).join("System32");
let powershell_directory = system_directory.join("WindowsPowerShell").join("v1.0");
executable_path.parent().is_some_and(|parent| {
parent
.as_os_str()
.eq_ignore_ascii_case(system_directory.as_os_str())
|| parent
.as_os_str()
.eq_ignore_ascii_case(powershell_directory.as_os_str())
})
});

#[cfg(not(windows))]
let is_system_shell = executable_path
.parent()
.is_some_and(|parent| parent == Path::new("/bin") || parent == Path::new("/usr/bin"));

#[cfg(windows)]
let is_configured_shell = executable_path
.as_os_str()
.eq_ignore_ascii_case(configured_shell.shell_path.as_os_str());

#[cfg(not(windows))]
let is_configured_shell = executable_path == configured_shell.shell_path.as_path();

if is_configured_shell
|| is_system_shell
|| matches!(shell_mode, UnifiedExecShellMode::ZshFork(_))
{
command
} else {
// An unfamiliar executable can ignore its arguments, so evaluate the
// executable separately from any restrictions on its apparent command.
std::slice::from_ref(executable)
}
}

#[cfg(test)]
#[path = "executable_identity_tests.rs"]
mod tests;
72 changes: 72 additions & 0 deletions codex-rs/core/src/exec_policy/executable_identity_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use super::shell_approval_command;
use crate::shell::Shell;
use crate::shell::ShellType;
use codex_tools::UnifiedExecShellMode;
use pretty_assertions::assert_eq;
use std::path::PathBuf;

#[test]
fn parent_directory_traversal_is_not_a_trusted_system_shell() {
let (configured_executable, unfamiliar_executable) = if cfg!(windows) {
let system_root = std::env::var_os("SystemRoot").expect("Windows SystemRoot");
let system_directory = PathBuf::from(system_root).join("System32");
(
system_directory.join("cmd.exe"),
system_directory
.join("..")
.join("workspace")
.join("powershell.exe"),
)
} else {
(
PathBuf::from("/bin/sh"),
PathBuf::from("/bin/../workspace/bash"),
)
};
let shell = Shell {
shell_type: if cfg!(windows) {
ShellType::Cmd
} else {
ShellType::Sh
},
shell_path: configured_executable,
};
let command = vec![
unfamiliar_executable.to_string_lossy().into_owned(),
"-c".to_string(),
"ls".to_string(),
];

assert_eq!(
shell_approval_command(&command, &shell, &UnifiedExecShellMode::Direct),
&command[..1],
);
}

#[cfg(windows)]
#[test]
fn windows_shell_identity_is_case_insensitive() {
let configured_executable = PathBuf::from(r"C:\Custom\pwsh.exe");
let configured_shell = Shell {
shell_type: ShellType::PowerShell,
shell_path: configured_executable.clone(),
};
let system_root = std::env::var_os("SystemRoot").expect("Windows SystemRoot");
let system_executable = PathBuf::from(system_root.to_string_lossy().to_ascii_uppercase())
.join("SYSTEM32")
.join("WINDOWSPOWERSHELL")
.join("V1.0")
.join("POWERSHELL.EXE");

for executable in [
configured_executable.to_string_lossy().to_ascii_uppercase(),
system_executable.to_string_lossy().into_owned(),
] {
let command = vec![executable, "-Command".to_string(), "echo safe".to_string()];

assert_eq!(
shell_approval_command(&command, &configured_shell, &UnifiedExecShellMode::Direct),
command.as_slice(),
);
}
}
1 change: 1 addition & 0 deletions codex-rs/core/src/tools/approvals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ impl ApprovalAction {
..
} => vec![ApprovalCacheKey::ExecCommand(UnifiedExecApprovalKey {
environment_id: environment_id.clone(),
executable: command.first().cloned(),
command: canonicalize_command_for_approval(command),
cwd: cwd.clone(),
tty: *tty,
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/tools/runtimes/unified_exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ pub struct UnifiedExecRequest {
#[derive(serde::Serialize, Clone, Debug, Eq, PartialEq, Hash)]
pub struct UnifiedExecApprovalKey {
pub environment_id: String,
pub executable: Option<String>,
pub command: Vec<String>,
pub cwd: PathUri,
pub tty: bool,
Expand Down
36 changes: 23 additions & 13 deletions codex-rs/core/src/unified_exec/process_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1238,24 +1238,34 @@ impl UnifiedExecProcessManager {
};
let mut orchestrator = ToolOrchestrator::new();
let mut runtime = UnifiedExecRuntime::new(self, request.shell_mode.clone());
let session_shell = context.session.user_shell();
let configured_shell = request
.turn_environment
.shell
.as_ref()
.unwrap_or(session_shell.as_ref());
let exec_approval_requirement = context
.session
.services
.exec_policy
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
command: &request.command,
approval_policy: turn.approval_policy(),
permission_profile: request.turn_environment.permission_profile().clone(),
environment_policy: request.turn_environment.config().exec_policy.as_ref(),
windows_sandbox_level: turn.windows_sandbox_level,
sandbox_permissions: if request.additional_permissions_preapproved {
crate::sandboxing::SandboxPermissions::UseDefault
} else {
request.sandbox_permissions
.create_exec_approval_requirement_for_shell(
ExecApprovalRequest {
command: &request.command,
approval_policy: turn.approval_policy(),
permission_profile: request.turn_environment.permission_profile().clone(),
environment_policy: request.turn_environment.config().exec_policy.as_ref(),
windows_sandbox_level: turn.windows_sandbox_level,
sandbox_permissions: if request.additional_permissions_preapproved {
crate::sandboxing::SandboxPermissions::UseDefault
} else {
request.sandbox_permissions
},
prefix_rule: request.prefix_rule.clone(),
allow_prefix_rules: context.step_context.turn.allow_prefix_rules(),
},
prefix_rule: request.prefix_rule.clone(),
allow_prefix_rules: context.step_context.turn.allow_prefix_rules(),
})
configured_shell,
&request.shell_mode,
)
.await;
let req = UnifiedExecToolRequest {
command: request.command.clone(),
Expand Down
Loading
Loading