pub const HARD_DENY_PREFIXES: &[&str] = &[
"AWS_",
"GCP_",
"GOOGLE_",
"AZURE_",
"OPENAI_",
"ANTHROPIC_",
"CLAUDE_",
"CODEX_",
"GH_",
"GITHUB_",
"GITLAB_",
"NPM_",
"PYPI_",
"DOCKER_",
"KUBE",
"VAULT_",
"SECRET",
"TOKEN",
"PRIVATE",
"CREDENTIAL",
"LDAP_",
"DB_",
"DATABASE_",
"STRIPE_",
"SENDGRID_",
"SLACK_",
"DISCORD_",
"TELEGRAM_",
"TWILIO_",
"API_KEY",
"AUTH_",
"SESSION_",
"COOKIE_",
"BEARER",
"BASIC_",
"OAUTH",
];
pub fn is_hard_denied(name: &str) -> bool {
let upper = name.to_uppercase();
HARD_DENY_PREFIXES.iter().any(|p| upper.starts_with(p))
|| upper.contains("SECRET")
|| upper.contains("PRIVATE_KEY")
|| upper.contains("CREDENTIALS")
}
pub fn sanitize_path(path: &str) -> String {
let mut seen = std::collections::BTreeSet::new();
let mut out = Vec::new();
for comp in path.split(':') {
if comp.is_empty() || comp == "." || comp.starts_with('~') {
continue;
}
if seen.insert(comp.to_string()) {
out.push(comp);
}
}
if out.is_empty() {
return "/usr/bin:/bin".to_string();
}
out.join(":")
}
pub fn filter_env(
vars: impl Iterator<Item = (String, String)>,
strict_path: bool,
) -> Vec<(String, String)> {
let mut out = Vec::new();
for (name, value) in vars {
if name.is_empty() || name.contains('=') || name.contains('\0') {
continue;
}
if value.contains('\0') {
continue;
}
if is_hard_denied(&name) {
continue;
}
if strict_path && name.eq_ignore_ascii_case("PATH") {
out.push((name, sanitize_path(&value)));
} else {
out.push((name, value));
}
}
out
}
#[cfg(test)]
mod envfilter_tests {
use super::*;
#[test]
fn hard_deny_catches_lowercase_aws() {
assert!(is_hard_denied("aws_secret_access_key"));
assert!(is_hard_denied("GH_TOKEN"));
assert!(!is_hard_denied("PATH"));
assert!(!is_hard_denied("HOME"));
}
#[test]
fn path_sanitizer_cleans() {
assert_eq!(sanitize_path("/a::.:~/x:/a"), "/a");
assert_eq!(sanitize_path(""), "/usr/bin:/bin");
assert_eq!(sanitize_path("/b:/a:/b"), "/b:/a");
}
#[test]
fn filter_env_drops_secrets_and_nul() {
let vars = vec![
("SECRET_FOO".to_string(), "x".to_string()),
("PATH".to_string(), "/a::/b".to_string()),
("BAD\0NAME".to_string(), "x".to_string()),
("OK".to_string(), "a\0b".to_string()),
("HOME".to_string(), "/h".to_string()),
];
let got = filter_env(vars.into_iter(), true);
assert_eq!(
got,
vec![
("PATH".to_string(), "/a:/b".to_string()),
("HOME".to_string(), "/h".to_string()),
]
);
}
}