pub(crate) fn is_dangerous_env_var(key: &str) -> bool {
key.starts_with("LD_")
|| key.starts_with("DYLD_")
|| key == "BASH_ENV"
|| key == "ENV"
|| key == "CDPATH"
|| key == "GLOBIGNORE"
|| key.starts_with("BASH_FUNC_")
|| key == "PROMPT_COMMAND"
|| key == "IFS"
|| key == "PYTHONSTARTUP"
|| key == "PYTHONPATH"
|| key == "NODE_OPTIONS"
|| key == "NODE_PATH"
|| key == "PERL5OPT"
|| key == "PERL5LIB"
|| key == "RUBYOPT"
|| key == "RUBYLIB"
|| key == "GEM_PATH"
|| key == "GEM_HOME"
|| key == "JAVA_TOOL_OPTIONS"
|| key == "_JAVA_OPTIONS"
|| key == "DOTNET_STARTUP_HOOKS"
|| key == "GOFLAGS"
|| key == "OP_SERVICE_ACCOUNT_TOKEN"
|| key == "OP_CONNECT_TOKEN"
|| key == "OP_CONNECT_HOST"
|| key.starts_with("OP_SESSION_")
}
fn matches_env_var_patterns(key: &str, patterns: &[String]) -> bool {
for pattern in patterns {
if let Some(prefix) = pattern.strip_suffix('*') {
if prefix.contains('*') {
continue;
}
if key.starts_with(prefix) {
return true;
}
} else if !pattern.contains('*') && key == *pattern {
return true;
}
}
false
}
pub(crate) fn is_env_var_allowed(key: &str, allowed_env_vars: &[String]) -> bool {
matches_env_var_patterns(key, allowed_env_vars)
}
pub(crate) fn is_env_var_denied(key: &str, denied_env_vars: &[String]) -> bool {
matches_env_var_patterns(key, denied_env_vars)
}
pub(crate) fn validate_env_var_patterns(patterns: &[String], field_name: &str) -> Option<String> {
for pattern in patterns {
if pattern.contains('*') && !pattern.ends_with('*') {
return Some(format!(
"Invalid {} pattern '{}': '*' is only valid as a trailing suffix",
field_name, pattern
));
}
if pattern.starts_with('*') && pattern.len() > 1 {
return Some(format!(
"Invalid {} pattern '{}': use a bare '*' to match all variables, or a specific prefix like 'AWS_*'",
field_name, pattern
));
}
}
None
}
pub(super) fn should_skip_env_var(
key: &str,
config_env_vars: &[(&str, &str)],
blocked_extra: &[&str],
) -> bool {
config_env_vars.iter().any(|(ek, _)| *ek == key)
|| blocked_extra.contains(&key)
|| is_dangerous_env_var(key)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_blocks_op_service_account_token() {
assert!(is_dangerous_env_var("OP_SERVICE_ACCOUNT_TOKEN"));
}
#[test]
fn test_blocks_op_connect_token() {
assert!(is_dangerous_env_var("OP_CONNECT_TOKEN"));
}
#[test]
fn test_blocks_op_connect_host() {
assert!(is_dangerous_env_var("OP_CONNECT_HOST"));
}
#[test]
fn test_blocks_op_session_prefix() {
assert!(is_dangerous_env_var("OP_SESSION_my_team"));
assert!(is_dangerous_env_var("OP_SESSION_personal"));
assert!(is_dangerous_env_var("OP_SESSION_"));
}
#[test]
fn test_allows_unrelated_env_vars() {
assert!(!is_dangerous_env_var("OPENAI_API_KEY"));
assert!(!is_dangerous_env_var("OPERATOR_TOKEN"));
assert!(!is_dangerous_env_var("OPTIONS"));
assert!(!is_dangerous_env_var("HOME"));
assert!(!is_dangerous_env_var("PATH"));
}
#[test]
fn test_blocks_linker_injection() {
assert!(is_dangerous_env_var("LD_PRELOAD"));
assert!(is_dangerous_env_var("DYLD_INSERT_LIBRARIES"));
}
#[test]
fn test_blocks_interpreter_injection() {
assert!(is_dangerous_env_var("NODE_OPTIONS"));
assert!(is_dangerous_env_var("PYTHONPATH"));
assert!(is_dangerous_env_var("RUBYOPT"));
}
#[test]
fn test_env_var_allowed_exact_match() {
let allowed: Vec<String> = vec!["PATH".into(), "HOME".into()];
assert!(is_env_var_allowed("PATH", &allowed));
assert!(is_env_var_allowed("HOME", &allowed));
}
#[test]
fn test_env_var_allowed_exact_no_match() {
let allowed: Vec<String> = vec!["PATH".into(), "HOME".into()];
assert!(!is_env_var_allowed("SECRET", &allowed));
}
#[test]
fn test_env_var_allowed_prefix_match() {
let allowed: Vec<String> = vec!["AWS_*".into()];
assert!(is_env_var_allowed("AWS_REGION", &allowed));
assert!(is_env_var_allowed("AWS_SECRET_ACCESS_KEY", &allowed));
}
#[test]
fn test_env_var_allowed_prefix_no_match() {
let allowed: Vec<String> = vec!["AWS_*".into()];
assert!(!is_env_var_allowed("GCP_REGION", &allowed));
}
#[test]
fn test_env_var_allowed_empty_list() {
let allowed: Vec<String> = vec![];
assert!(!is_env_var_allowed("PATH", &allowed));
}
#[test]
fn test_env_var_allowed_bare_star() {
let allowed: Vec<String> = vec!["*".into()];
assert!(is_env_var_allowed("ANYTHING", &allowed));
assert!(is_env_var_allowed("PATH", &allowed));
}
#[test]
fn test_env_var_allowed_prefix_does_not_match_partial() {
let allowed: Vec<String> = vec!["AWS_*".into()];
assert!(!is_env_var_allowed("AWS", &allowed));
}
#[test]
fn test_env_var_allowed_prefix_matches_empty_suffix() {
let allowed: Vec<String> = vec!["AWS_*".into()];
assert!(is_env_var_allowed("AWS_", &allowed));
}
#[test]
fn test_env_var_allowed_mixed_patterns() {
let allowed: Vec<String> = vec!["PATH".into(), "AWS_*".into()];
assert!(is_env_var_allowed("PATH", &allowed));
assert!(is_env_var_allowed("AWS_REGION", &allowed));
assert!(!is_env_var_allowed("HOME", &allowed));
}
#[test]
fn test_env_var_allowed_mid_star_ignored() {
let allowed: Vec<String> = vec!["A*B".into()];
assert!(!is_env_var_allowed("AXB", &allowed));
assert!(!is_env_var_allowed("A*B", &allowed));
}
#[test]
fn test_validate_valid_patterns() {
let patterns: Vec<String> = vec!["PATH".into(), "AWS_*".into(), "*".into()];
assert!(validate_env_var_patterns(&patterns, "allow_vars").is_none());
}
#[test]
fn test_validate_rejects_mid_star() {
let patterns: Vec<String> = vec!["A*B".into()];
let err = validate_env_var_patterns(&patterns, "allow_vars");
assert!(err.is_some());
assert!(err.as_ref().is_some_and(|e| e.contains("A*B")));
}
#[test]
fn test_validate_rejects_leading_star_with_suffix() {
let patterns: Vec<String> = vec!["*X".into()];
let err = validate_env_var_patterns(&patterns, "allow_vars");
assert!(err.is_some());
assert!(err.as_ref().is_some_and(|e| e.contains("*X")));
}
#[test]
fn test_validate_accepts_bare_star() {
let patterns: Vec<String> = vec!["*".into()];
assert!(validate_env_var_patterns(&patterns, "allow_vars").is_none());
}
#[test]
fn test_validate_exact_name_no_star() {
let patterns: Vec<String> = vec!["PATH".into()];
assert!(validate_env_var_patterns(&patterns, "allow_vars").is_none());
}
#[test]
fn test_validate_deny_vars_field_name_in_error() {
let patterns: Vec<String> = vec!["A*B".into()];
let err = validate_env_var_patterns(&patterns, "deny_vars");
assert!(err.as_ref().is_some_and(|e| e.contains("deny_vars")));
assert!(err.as_ref().is_some_and(|e| e.contains("A*B")));
}
#[test]
fn test_env_var_denied_exact_match() {
let denied: Vec<String> = vec!["GH_TOKEN".into(), "ANTHROPIC_API_KEY".into()];
assert!(is_env_var_denied("GH_TOKEN", &denied));
assert!(is_env_var_denied("ANTHROPIC_API_KEY", &denied));
}
#[test]
fn test_env_var_denied_prefix_match() {
let denied: Vec<String> = vec!["GITHUB_*".into()];
assert!(is_env_var_denied("GITHUB_TOKEN", &denied));
assert!(is_env_var_denied("GITHUB_ACTIONS", &denied));
assert!(!is_env_var_denied("GH_TOKEN", &denied));
}
#[test]
fn test_env_var_denied_no_match() {
let denied: Vec<String> = vec!["GH_TOKEN".into()];
assert!(!is_env_var_denied("PATH", &denied));
assert!(!is_env_var_denied("HOME", &denied));
}
#[test]
fn test_env_var_denied_empty_list() {
let denied: Vec<String> = vec![];
assert!(!is_env_var_denied("GH_TOKEN", &denied));
}
#[test]
fn test_env_var_denied_overrides_allowed() {
let denied: Vec<String> = vec!["GH_TOKEN".into()];
let allowed: Vec<String> = vec!["GH_TOKEN".into()];
assert!(is_env_var_denied("GH_TOKEN", &denied));
assert!(is_env_var_allowed("GH_TOKEN", &allowed));
}
}