use std::collections::HashMap;
use crate::error::Error;
pub(crate) const INJECT_ENV_VARS: &[&str] = &[
"LD_PRELOAD",
"LD_PRELOAD_32",
"LD_PRELOAD_64",
"LD_AUDIT",
"DYLD_INSERT_LIBRARIES",
];
pub(crate) const SUSPICIOUS_ENV_VARS: &[&str] = &[
"LD_LIBRARY_PATH",
"LD_DEBUG",
"LD_DEBUG_OUTPUT",
"LD_DYNAMIC_WEAK",
"LD_ORIGIN_PATH",
"LD_PROFILE",
"LD_SHOW_AUXV",
"DYLD_LIBRARY_PATH",
"DYLD_FRAMEWORK_PATH",
"DYLD_FALLBACK_LIBRARY_PATH",
"PYTHONPATH",
"BASH_ENV",
"ENV",
"NODE_OPTIONS",
"RUBYOPT",
"PERL5LIB",
"JAVA_TOOL_OPTIONS",
"GLIBC_TUNABLES",
"GCONV_PATH",
"LOCPATH",
"PERL5OPT",
"JDK_JAVA_OPTIONS",
"_JAVA_OPTIONS",
"PYTHONSTARTUP",
"PYTHONHOME",
"PYTHONUSERBASE",
"OPENSSL_CONF",
"OPENSSL_MODULES",
];
pub(crate) const HOSTILE_VAR_PREFIXES: &[&str] = &[
"BASH_FUNC_",
];
#[derive(Debug, PartialEq)]
pub enum EnvironmentThreatLevel {
Safe,
Degraded(String),
Hostile(String),
}
#[must_use]
pub fn assess_environment() -> EnvironmentThreatLevel {
let mut hostile = Vec::new();
for var in INJECT_ENV_VARS {
if std::env::var_os(var).is_some() {
hostile.push(*var);
}
}
if !hostile.is_empty() {
return EnvironmentThreatLevel::Hostile(format!(
"direct injection variables detected: {}",
hostile.join(", ")
));
}
let mut degraded = Vec::new();
for var in SUSPICIOUS_ENV_VARS {
if std::env::var_os(var).is_some() {
degraded.push(*var);
}
}
if !degraded.is_empty() {
return EnvironmentThreatLevel::Degraded(format!(
"suspicious environment variables set: {}",
degraded.join(", ")
));
}
EnvironmentThreatLevel::Safe
}
#[must_use]
pub fn assess_env_signals(_ctx: &super::DetectorContext) -> Vec<super::Signal> {
let mut signals = Vec::new();
for var in INJECT_ENV_VARS {
if std::env::var_os(var).is_some() {
signals.push(super::Signal::new(
super::SignalId::scoped("env.preload", var),
super::Category::EnvironmentInjection,
super::Severity::Hostile,
"dynamic-loader injection variable set",
format!("`{var}` is set in our parent environment — every child we exec will load attacker-controlled code"),
"unset the variable before invoking envseal, or run from a clean shell",
));
}
}
for var in SUSPICIOUS_ENV_VARS {
if std::env::var_os(var).is_some() {
signals.push(super::Signal::new(
super::SignalId::scoped("env.suspicious_loader_var", var),
super::Category::EnvironmentInjection,
super::Severity::Degraded,
"suspicious loader configuration variable set",
format!("`{var}` modifies library resolution"),
"review whether this var is intentional in this shell",
));
}
}
signals
}
pub fn enforce_env_policy(config: &crate::security_config::SecurityConfig) -> Result<(), Error> {
let ctx = super::DetectorContext::ambient();
let signals = assess_env_signals(&ctx);
super::emit_signals_inline(signals, config)
}
#[must_use]
pub fn sanitized_env() -> HashMap<String, String> {
let blocked: std::collections::HashSet<&str> = INJECT_ENV_VARS
.iter()
.chain(SUSPICIOUS_ENV_VARS.iter())
.copied()
.collect();
std::env::vars()
.filter(|(key, _)| {
if blocked.contains(key.as_str()) {
return false;
}
if HOSTILE_VAR_PREFIXES
.iter()
.any(|prefix| key.starts_with(prefix))
{
return false;
}
!key.starts_with("ENVSEAL_")
})
.collect()
}
#[cfg(test)]
mod sanitized_env_tests {
use super::sanitized_env;
#[test]
fn strips_bash_func_prefixed_exports() {
let key = "BASH_FUNC_envseal_test_marker%%";
std::env::set_var(key, "() { echo gotcha; }");
let env = sanitized_env();
let leaked = env.contains_key(key);
std::env::remove_var(key);
assert!(!leaked, "BASH_FUNC_-prefixed export survived sanitized_env");
}
#[test]
fn strips_pythonstartup_and_openssl_conf() {
std::env::set_var("PYTHONSTARTUP", "/tmp/marker.py");
std::env::set_var("OPENSSL_CONF", "/tmp/marker.cnf");
let env = sanitized_env();
let py = env.contains_key("PYTHONSTARTUP");
let ssl = env.contains_key("OPENSSL_CONF");
std::env::remove_var("PYTHONSTARTUP");
std::env::remove_var("OPENSSL_CONF");
assert!(!py, "PYTHONSTARTUP survived sanitized_env");
assert!(!ssl, "OPENSSL_CONF survived sanitized_env");
}
#[test]
fn strips_envseal_internal_vars() {
std::env::set_var("ENVSEAL_TEST_MARKER", "leaked");
std::env::set_var("ENVSEAL_MASTER_KEY_PATH", "/secret");
let env = sanitized_env();
let leaked1 = env.contains_key("ENVSEAL_TEST_MARKER");
let leaked2 = env.contains_key("ENVSEAL_MASTER_KEY_PATH");
std::env::remove_var("ENVSEAL_TEST_MARKER");
std::env::remove_var("ENVSEAL_MASTER_KEY_PATH");
assert!(!leaked1, "ENVSEAL_TEST_MARKER survived sanitized_env");
assert!(!leaked2, "ENVSEAL_MASTER_KEY_PATH survived sanitized_env");
}
}