mirror of
https://github.com/openai/codex.git
synced 2026-05-24 13:04:29 +00:00
253 lines
7.3 KiB
Rust
253 lines
7.3 KiB
Rust
use crate::bash::parse_shell_lc_plain_commands;
|
|
use crate::command_safety::ripgrep::RipgrepArgCase;
|
|
use crate::command_safety::ripgrep::ripgrep_command_can_execute_arbitrary_command;
|
|
use std::path::Path;
|
|
#[cfg(windows)]
|
|
#[path = "windows_dangerous_commands.rs"]
|
|
mod windows_dangerous_commands;
|
|
|
|
pub fn command_might_be_dangerous(command: &[String]) -> bool {
|
|
#[cfg(windows)]
|
|
{
|
|
if windows_dangerous_commands::is_dangerous_command_windows(command) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if is_dangerous_to_call_with_exec(command) {
|
|
return true;
|
|
}
|
|
|
|
// Support `bash -lc "<script>"` where the any part of the script might contain a dangerous command.
|
|
if let Some(all_commands) = parse_shell_lc_plain_commands(command)
|
|
&& all_commands
|
|
.iter()
|
|
.any(|cmd| is_dangerous_to_call_with_exec(cmd))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
pub fn command_can_execute_arbitrary_command(command: &[String]) -> bool {
|
|
if direct_command_can_execute_arbitrary_command(command) {
|
|
return true;
|
|
}
|
|
|
|
if let Some(all_commands) = parse_shell_lc_plain_commands(command)
|
|
&& all_commands
|
|
.iter()
|
|
.any(|cmd| direct_command_can_execute_arbitrary_command(cmd))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
/// Returns whether already-tokenized PowerShell words should be treated as
|
|
/// dangerous by the Windows unmatched-command heuristics.
|
|
pub fn is_dangerous_powershell_words(command: &[String]) -> bool {
|
|
#[cfg(windows)]
|
|
{
|
|
windows_dangerous_commands::is_dangerous_powershell_words(command)
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
{
|
|
let _ = command;
|
|
false
|
|
}
|
|
}
|
|
|
|
fn is_git_global_option_with_value(arg: &str) -> bool {
|
|
matches!(
|
|
arg,
|
|
"-C" | "-c"
|
|
| "--config-env"
|
|
| "--exec-path"
|
|
| "--git-dir"
|
|
| "--namespace"
|
|
| "--super-prefix"
|
|
| "--work-tree"
|
|
)
|
|
}
|
|
|
|
fn is_git_global_option_with_inline_value(arg: &str) -> bool {
|
|
matches!(
|
|
arg,
|
|
s if s.starts_with("--config-env=")
|
|
|| s.starts_with("--exec-path=")
|
|
|| s.starts_with("--git-dir=")
|
|
|| s.starts_with("--namespace=")
|
|
|| s.starts_with("--super-prefix=")
|
|
|| s.starts_with("--work-tree=")
|
|
) || ((arg.starts_with("-C") || arg.starts_with("-c")) && arg.len() > 2)
|
|
}
|
|
|
|
pub(crate) fn executable_name_lookup_key(raw: &str) -> Option<String> {
|
|
#[cfg(windows)]
|
|
{
|
|
Path::new(raw)
|
|
.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.map(|name| {
|
|
let name = name.to_ascii_lowercase();
|
|
for suffix in [".exe", ".cmd", ".bat", ".com"] {
|
|
if let Some(stripped) = name.strip_suffix(suffix) {
|
|
return stripped.to_string();
|
|
}
|
|
}
|
|
name
|
|
})
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
{
|
|
Path::new(raw)
|
|
.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.map(std::borrow::ToOwned::to_owned)
|
|
}
|
|
}
|
|
|
|
/// Find the first matching git subcommand, skipping known global options that
|
|
/// may appear before it (e.g., `-C`, `-c`, `--git-dir`).
|
|
///
|
|
/// Shared with `is_safe_command` to avoid git-global-option bypasses.
|
|
pub(crate) fn find_git_subcommand<'a>(
|
|
command: &'a [String],
|
|
subcommands: &[&str],
|
|
) -> Option<(usize, &'a str)> {
|
|
let cmd0 = command.first().map(String::as_str)?;
|
|
if executable_name_lookup_key(cmd0).as_deref() != Some("git") {
|
|
return None;
|
|
}
|
|
|
|
let mut skip_next = false;
|
|
for (idx, arg) in command.iter().enumerate().skip(1) {
|
|
if skip_next {
|
|
skip_next = false;
|
|
continue;
|
|
}
|
|
|
|
let arg = arg.as_str();
|
|
|
|
if is_git_global_option_with_inline_value(arg) {
|
|
continue;
|
|
}
|
|
|
|
if is_git_global_option_with_value(arg) {
|
|
skip_next = true;
|
|
continue;
|
|
}
|
|
|
|
if arg == "--" || arg.starts_with('-') {
|
|
continue;
|
|
}
|
|
|
|
if subcommands.contains(&arg) {
|
|
return Some((idx, arg));
|
|
}
|
|
|
|
// In git, the first non-option token is the subcommand. If it isn't
|
|
// one of the subcommands we're looking for, we must stop scanning to
|
|
// avoid misclassifying later positional args (e.g., branch names).
|
|
return None;
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn is_dangerous_to_call_with_exec(command: &[String]) -> bool {
|
|
let cmd0 = command
|
|
.first()
|
|
.and_then(|command| executable_name_lookup_key(command));
|
|
|
|
match cmd0.as_deref() {
|
|
Some("rm") => matches!(command.get(1).map(String::as_str), Some("-f" | "-rf")),
|
|
|
|
// for sudo <cmd> simply do the check for <cmd>
|
|
Some("sudo") => is_dangerous_to_call_with_exec(&command[1..]),
|
|
|
|
Some("rg") => direct_command_can_execute_arbitrary_command(command),
|
|
|
|
// ── anything else ─────────────────────────────────────────────────
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn direct_command_can_execute_arbitrary_command(command: &[String]) -> bool {
|
|
let cmd0 = command
|
|
.first()
|
|
.and_then(|command| executable_name_lookup_key(command));
|
|
|
|
match cmd0.as_deref() {
|
|
Some("sudo") => direct_command_can_execute_arbitrary_command(&command[1..]),
|
|
Some("rg") => {
|
|
ripgrep_command_can_execute_arbitrary_command(command, RipgrepArgCase::Sensitive)
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn vec_str(items: &[&str]) -> Vec<String> {
|
|
items.iter().map(std::string::ToString::to_string).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn rm_rf_is_dangerous() {
|
|
assert!(command_might_be_dangerous(&vec_str(&["rm", "-rf", "/"])));
|
|
}
|
|
|
|
#[test]
|
|
fn rm_f_is_dangerous() {
|
|
assert!(command_might_be_dangerous(&vec_str(&["rm", "-f", "/"])));
|
|
}
|
|
|
|
#[test]
|
|
fn ripgrep_pre_processor_is_dangerous() {
|
|
for command in [
|
|
vec_str(&["rg", "--pre", "./pre.sh", "needle", "input.txt"]),
|
|
vec_str(&["rg", "--pre=./pre.sh", "needle", "input.txt"]),
|
|
vec_str(&["/usr/bin/rg", "--hostname-bin=./hostname.sh", "needle"]),
|
|
vec_str(&["zsh", "-c", r"rg --pre\=./pre.sh needle input.txt"]),
|
|
vec_str(&["zsh", "-lc", r"rg --pre\=./pre.sh needle input.txt"]),
|
|
vec_str(&["/bin/zsh", "-lc", "rg --pre=./pre.sh needle input.txt"]),
|
|
] {
|
|
assert!(
|
|
command_might_be_dangerous(&command),
|
|
"expected {command:?} to be dangerous",
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn ripgrep_search_zip_is_not_dangerous() {
|
|
assert!(!command_might_be_dangerous(&vec_str(&[
|
|
"rg",
|
|
"--search-zip",
|
|
"needle",
|
|
])));
|
|
assert!(!command_might_be_dangerous(&vec_str(&[
|
|
"rg", "-z", "needle",
|
|
])));
|
|
}
|
|
|
|
#[test]
|
|
fn direct_powershell_words_reuse_windows_dangerous_detection() {
|
|
let command = vec_str(&["Remove-Item", "test", "-Force"]);
|
|
|
|
if cfg!(windows) {
|
|
assert!(is_dangerous_powershell_words(&command));
|
|
} else {
|
|
assert!(!is_dangerous_powershell_words(&command));
|
|
}
|
|
}
|
|
}
|