use std::ffi::OsString;
use std::process::Command;
const SECRET_ENV_KEYS: &[&str] = &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "MORPH_API_KEY"];
const LOADER_KEEP_ENV_KEYS: &[&str] = &["LD_LIBRARY_PATH", "LD_RUN_PATH", "DYLD_LIBRARY_PATH"];
fn is_loader_injection_var(key: &str) -> bool {
(key.starts_with("LD_") || key.starts_with("DYLD_")) && !LOADER_KEEP_ENV_KEYS.contains(&key)
}
pub(crate) fn scrub_sensitive_env(cmd: &mut Command) {
scrub_env_from(cmd, std::env::vars_os());
}
fn scrub_env_from(cmd: &mut Command, inherited: impl Iterator<Item = (OsString, OsString)>) {
for key in SECRET_ENV_KEYS {
cmd.env_remove(key);
}
for (key, _) in inherited {
if is_loader_injection_var(&key.to_string_lossy()) {
cmd.env_remove(&key);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn removed_keys(cmd: &Command) -> Vec<String> {
cmd.get_envs()
.filter(|(_, value)| value.is_none())
.map(|(key, _)| key.to_string_lossy().into_owned())
.collect()
}
fn fake_env(keys: &[&str]) -> Vec<(OsString, OsString)> {
keys.iter()
.map(|k| (OsString::from(k), OsString::from("x")))
.collect()
}
#[test]
fn loader_predicate_strips_the_family_but_keeps_build_paths() {
for key in [
"LD_PRELOAD",
"LD_AUDIT",
"LD_DEBUG",
"DYLD_INSERT_LIBRARIES",
"DYLD_FRAMEWORK_PATH",
"DYLD_FALLBACK_FRAMEWORK_PATH",
"DYLD_FORCE_FLAT_NAMESPACE",
"DYLD_VERSIONED_LIBRARY_PATH",
] {
assert!(is_loader_injection_var(key), "{key} should be stripped");
}
for key in [
"LD_LIBRARY_PATH",
"LD_RUN_PATH",
"DYLD_LIBRARY_PATH",
"PATH",
"LDFLAGS",
"HOME",
] {
assert!(!is_loader_injection_var(key), "{key} should be kept");
}
}
#[test]
fn scrub_removes_secrets_and_the_loader_family() {
let inherited = fake_env(&[
"DYLD_INSERT_LIBRARIES",
"DYLD_FRAMEWORK_PATH",
"DYLD_FALLBACK_FRAMEWORK_PATH",
"DYLD_FORCE_FLAT_NAMESPACE",
"DYLD_LIBRARY_PATH",
"LD_PRELOAD",
"LD_AUDIT",
"LD_DEBUG",
"LD_LIBRARY_PATH",
"LD_RUN_PATH",
"PATH",
"HOME",
]);
let mut cmd = Command::new("true");
scrub_env_from(&mut cmd, inherited.into_iter());
let removed = removed_keys(&cmd);
for key in SECRET_ENV_KEYS {
assert!(removed.contains(&key.to_string()), "{key} must be removed");
}
for key in [
"DYLD_INSERT_LIBRARIES",
"DYLD_FRAMEWORK_PATH",
"DYLD_FALLBACK_FRAMEWORK_PATH",
"DYLD_FORCE_FLAT_NAMESPACE",
"LD_PRELOAD",
"LD_AUDIT",
"LD_DEBUG",
] {
assert!(removed.contains(&key.to_string()), "{key} must be removed");
}
for key in [
"DYLD_LIBRARY_PATH",
"LD_LIBRARY_PATH",
"LD_RUN_PATH",
"PATH",
"HOME",
] {
assert!(!removed.contains(&key.to_string()), "{key} must be kept");
}
}
#[test]
fn scrub_list_covers_every_credential_argument() {
use clap::CommandFactory;
const CREDENTIAL_MARKERS: [&str; 6] =
["KEY", "SECRET", "TOKEN", "PASSWORD", "PASSWD", "CREDENTIAL"];
for arg in crate::cli::Cli::command().get_arguments() {
let Some(env) = arg.get_env() else { continue };
let env = env.to_string_lossy();
let upper = env.to_uppercase();
if CREDENTIAL_MARKERS
.iter()
.any(|marker| upper.contains(marker))
{
assert!(
SECRET_ENV_KEYS.contains(&env.as_ref()),
"{env} looks like a credential argument in cli.rs but is not scrubbed from child processes"
);
}
}
}
}