#[must_use]
pub fn constant_time_eq(a: &str, b: &str) -> bool {
let (a, b) = (a.as_bytes(), b.as_bytes());
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b) {
diff |= x ^ y;
}
diff == 0
}
#[must_use]
pub fn redact(value: &str) -> String {
let chars: Vec<char> = value.chars().collect();
if chars.len() <= 8 {
return "****".to_string();
}
format!(
"****{}",
chars[chars.len() - 4..].iter().collect::<String>()
)
}
#[must_use]
pub fn is_secret_header(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
["auth", "key", "token", "secret", "cookie", "credential"]
.iter()
.any(|hint| lower.contains(hint))
}
const CHILD_ENV_ALLOWLIST: &[&str] = &[
"PATH",
"HOME",
"SHELL",
"USER",
"LOGNAME",
"TMPDIR",
"TEMP",
"TMP",
"LANG",
"LANGUAGE",
"TERM",
"TZ",
"COLORTERM",
"NO_COLOR",
"SYSTEMROOT",
"WINDIR",
"COMSPEC",
"PATHEXT",
"APPDATA",
"LOCALAPPDATA",
"PROGRAMFILES",
"PROGRAMDATA",
"USERPROFILE",
"HOMEDRIVE",
"HOMEPATH",
"NUMBER_OF_PROCESSORS",
"PROCESSOR_ARCHITECTURE",
"OS",
];
pub fn child_env_allowed(name: &str) -> bool {
CHILD_ENV_ALLOWLIST
.iter()
.any(|allowed| allowed.eq_ignore_ascii_case(name))
}
const SECRET_NAME_HINTS: &[&str] = &[
"TOKEN",
"SECRET",
"PASSWORD",
"PASSWD",
"PASSPHRASE",
"CREDENTIAL",
"APIKEY",
"API_KEY",
"ACCESS_KEY",
"PRIVATE_KEY",
"KEY",
"_PAT",
"SESSION",
"COOKIE",
"AUTH",
"BEARER",
"SIGNATURE",
"SIGNING",
"DSN",
"NETRC",
"KUBECONFIG",
];
const SECRET_NAME_EXACT: &[&str] = &[
"DATABASE_URL",
"DATABASE_DSN",
"REDIS_URL",
"MONGO_URL",
"MONGODB_URI",
"AMQP_URL",
"SSH_AUTH_SOCK",
];
const SECRET_NAME_PREFIXES: &[&str] = &[
"LEVIATH_", "AWS_", "AZURE_", "GOOGLE_", "GCP_",
];
const DOTENV_DENY_EXACT: &[&str] = &[
"EDITOR",
"VISUAL",
"PATH",
"SHELL",
"BASH_ENV",
"ENV",
"GIT_EXTERNAL_DIFF",
"GIT_SSH",
"GIT_SSH_COMMAND",
"GIT_PAGER",
"GIT_CONFIG_GLOBAL",
"GIT_CONFIG_SYSTEM",
"PAGER",
"MANPAGER",
"LESSOPEN",
"LESSCLOSE",
"NODE_OPTIONS",
"PYTHONSTARTUP",
"PYTHONPATH",
"PERL5OPT",
"PERL5LIB",
"RUBYOPT",
"JAVA_TOOL_OPTIONS",
"_JAVA_OPTIONS",
"RUSTC_WRAPPER",
"RUSTC",
];
const DOTENV_DENY_PREFIXES: &[&str] = &[
"LEVIATH_", "LD_", "DYLD_",
];
#[must_use]
pub fn dotenv_var_allowed(name: &str) -> bool {
let upper = name.to_ascii_uppercase();
!DOTENV_DENY_EXACT.contains(&upper.as_str())
&& !DOTENV_DENY_PREFIXES.iter().any(|p| upper.starts_with(p))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ShellEnvMode {
#[default]
Filtered,
Strict,
Custom,
Inherit,
}
pub fn withheld_child_vars<'a>(
names: impl Iterator<Item = &'a str>,
mode: ShellEnvMode,
allow_env_vars: &[String],
custom_withhold: &[String],
) -> Vec<String> {
let listed = |list: &[String], name: &str| list.iter().any(|e| e.eq_ignore_ascii_case(name));
names
.filter(|name| match mode {
ShellEnvMode::Inherit => false,
ShellEnvMode::Custom => listed(custom_withhold, name) && !listed(allow_env_vars, name),
ShellEnvMode::Filtered if name.eq_ignore_ascii_case("SSH_AUTH_SOCK") => false,
ShellEnvMode::Filtered | ShellEnvMode::Strict => {
!script_env_allowed(name, allow_env_vars)
}
})
.map(str::to_string)
.collect()
}
pub fn is_sensitive_env_name(name: &str) -> bool {
let upper = name.to_ascii_uppercase();
SECRET_NAME_HINTS.iter().any(|h| upper.contains(h))
|| SECRET_NAME_EXACT.iter().any(|e| upper == *e)
|| SECRET_NAME_PREFIXES.iter().any(|p| upper.starts_with(p))
}
pub fn script_env_allowed(name: &str, allowlist: &[String]) -> bool {
!is_sensitive_env_name(name)
|| allowlist
.iter()
.any(|allowed| allowed.eq_ignore_ascii_case(name))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constant_time_eq_matches_ordinary_equality() {
assert!(constant_time_eq("secret", "secret"));
assert!(constant_time_eq("", ""));
assert!(!constant_time_eq("secret", "secreu"));
assert!(!constant_time_eq("Xecret", "secret"));
assert!(!constant_time_eq("secreX", "secret"));
assert!(!constant_time_eq("secret", "secretx"));
assert!(!constant_time_eq("secretx", "secret"));
}
#[test]
fn redact_keeps_only_a_short_suffix() {
assert_eq!(redact("sk-ant-api-key-12345"), "****2345");
assert!(!redact("sk-ant-api-key-12345").contains("sk-ant"));
assert!(!redact("ghp_realgithubtoken").contains("ghp_"));
}
#[test]
fn redact_hides_short_values_completely() {
for value in ["", "a", "abcd", "12345678"] {
assert_eq!(redact(value), "****", "{value:?}");
}
}
#[test]
fn redact_counts_characters_not_bytes() {
assert_eq!(redact("日本語日本語日本語"), "****語日本語");
assert_eq!(redact("日本"), "****");
}
#[test]
fn secret_headers_are_matched_by_shape_not_an_exact_list() {
for name in [
"authorization",
"Authorization",
"x-api-key",
"api-key",
"x-goog-api-key",
"proxy-authorization",
"cookie",
"set-cookie",
"x-auth-token",
"x-amz-security-token",
] {
assert!(is_secret_header(name), "{name} must be redacted");
}
}
#[test]
fn ordinary_headers_are_not_redacted() {
for name in [
"content-type",
"user-agent",
"accept",
"content-length",
"anthropic-version",
] {
assert!(!is_secret_header(name), "{name} should log verbatim");
}
}
#[test]
fn catches_what_the_old_denylist_missed() {
for name in [
"AWS_SECRET_ACCESS_KEY",
"AWS_SESSION_TOKEN",
"GITHUB_TOKEN",
"GH_TOKEN",
"NPM_TOKEN",
"HF_TOKEN",
"SLACK_TOKEN",
"DATABASE_URL",
"LEVIATH_API_TOKEN",
"SSH_AUTH_SOCK",
] {
assert!(is_sensitive_env_name(name), "{name} should be sensitive");
assert!(!child_env_allowed(name), "{name} must not reach a child");
}
}
const SAMPLE_ENV: &[&str] = &[
"ANTHROPIC_API_KEY",
"GITHUB_TOKEN",
"AWS_SECRET_ACCESS_KEY",
"LEVIATH_API_TOKEN",
"DATABASE_URL",
"SSH_AUTH_SOCK",
"KUBECONFIG",
"PATH",
"HOME",
"CARGO_HOME",
"JAVA_HOME",
"VIRTUAL_ENV",
"NVM_DIR",
"GOPATH",
"DOCKER_HOST",
"TERM",
];
fn withheld(mode: ShellEnvMode, allow: &[&str], custom: &[&str]) -> Vec<String> {
let allow: Vec<String> = allow.iter().map(|s| s.to_string()).collect();
let custom: Vec<String> = custom.iter().map(|s| s.to_string()).collect();
withheld_child_vars(SAMPLE_ENV.iter().copied(), mode, &allow, &custom)
}
#[test]
fn filtered_withholds_credentials_and_keeps_toolchains() {
let out = withheld(ShellEnvMode::Filtered, &[], &[]);
for secret in [
"ANTHROPIC_API_KEY",
"GITHUB_TOKEN",
"AWS_SECRET_ACCESS_KEY",
"LEVIATH_API_TOKEN",
"DATABASE_URL",
] {
assert!(out.iter().any(|n| n == secret), "{secret} must be withheld");
}
for kept in [
"SSH_AUTH_SOCK",
"PATH",
"HOME",
"CARGO_HOME",
"JAVA_HOME",
"VIRTUAL_ENV",
"NVM_DIR",
"GOPATH",
"DOCKER_HOST",
"TERM",
] {
assert!(!out.iter().any(|n| n == kept), "{kept} must pass through");
}
}
#[test]
fn strict_also_withholds_the_agent_socket() {
let out = withheld(ShellEnvMode::Strict, &[], &[]);
assert!(out.iter().any(|n| n == "SSH_AUTH_SOCK"));
assert!(out.iter().any(|n| n == "KUBECONFIG"));
assert!(!out.iter().any(|n| n == "PATH"));
assert!(!out.iter().any(|n| n == "CARGO_HOME"));
}
#[test]
fn custom_withholds_exactly_what_it_names() {
let out = withheld(ShellEnvMode::Custom, &[], &["home", "MY_UNUSUAL_NAME"]);
assert_eq!(out, ["HOME"], "case-insensitive, and nothing inferred");
assert!(
!out.iter().any(|n| n == "ANTHROPIC_API_KEY"),
"the heuristic is off, so a credential passes unless named"
);
}
#[test]
fn inherit_withholds_nothing() {
assert!(withheld(ShellEnvMode::Inherit, &[], &["PATH"]).is_empty());
}
#[test]
fn an_allowlisted_name_is_handed_over_under_every_mode() {
for mode in [
ShellEnvMode::Filtered,
ShellEnvMode::Strict,
ShellEnvMode::Custom,
] {
let out = withheld(mode, &["anthropic_api_key", "HOME"], &["HOME"]);
assert!(
!out.iter().any(|n| n == "ANTHROPIC_API_KEY"),
"{mode:?} should honour allow_env_vars"
);
assert!(!out.iter().any(|n| n == "HOME"), "{mode:?}");
}
}
#[test]
fn a_dot_env_may_not_steer_the_process() {
for name in [
"LEVIATH_CONFIG_PATH",
"LEVIATH_HOME",
"LEVIATH_API_TOKEN",
"LEVIATH_RUNS_DIR",
"PATH",
"SHELL",
"EDITOR",
"VISUAL",
"LD_PRELOAD",
"LD_LIBRARY_PATH",
"DYLD_INSERT_LIBRARIES",
"GIT_EXTERNAL_DIFF",
"GIT_SSH_COMMAND",
"GIT_CONFIG_GLOBAL",
"GIT_PAGER",
"BASH_ENV",
"PAGER",
"LESSOPEN",
"NODE_OPTIONS",
"PYTHONSTARTUP",
"PERL5OPT",
"RUBYOPT",
"JAVA_TOOL_OPTIONS",
"RUSTC_WRAPPER",
] {
assert!(
!dotenv_var_allowed(name),
"{name} must not be settable from a repository's .env"
);
assert!(!dotenv_var_allowed(&name.to_ascii_lowercase()));
}
}
#[test]
fn a_dot_env_may_still_carry_credentials_and_ordinary_config() {
for name in [
"ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
"GITHUB_TOKEN",
"DATABASE_URL",
"AWS_SECRET_ACCESS_KEY",
"OLLAMA_HOST",
"MY_APP_REGION",
"RUST_LOG",
] {
assert!(dotenv_var_allowed(name), "{name} should still load");
}
}
#[test]
fn catches_the_provider_keys() {
for name in [
"ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
"GOOGLE_API_KEY",
"OPENROUTER_API_KEY",
] {
assert!(is_sensitive_env_name(name), "{name}");
}
}
#[test]
fn common_secret_names_are_all_recognised() {
for name in [
"OPENAI_KEY",
"ENCRYPTION_KEY",
"MASTER_KEY",
"DEPLOY_KEY",
"ANTHROPIC_API_KEY",
"AWS_SECRET_ACCESS_KEY",
"GITHUB_TOKEN",
"GITHUB_PAT",
"SENTRY_DSN",
"NETRC",
"KUBECONFIG",
"npm_password",
] {
assert!(
is_sensitive_env_name(name),
"{name} must be treated as a secret"
);
}
for name in [
"PATH",
"HOME",
"LANG",
"TERM",
"TZ",
"EDITOR",
"OLLAMA_HOST",
] {
assert!(!is_sensitive_env_name(name), "{name} is not a secret");
}
}
#[test]
fn matching_is_case_insensitive() {
assert!(is_sensitive_env_name("github_token"));
assert!(is_sensitive_env_name("MyApp_Password"));
assert!(child_env_allowed("path"));
assert!(child_env_allowed("Path"));
}
#[test]
fn ordinary_names_are_not_sensitive() {
for name in ["PATH", "HOME", "TZ", "TERM", "EDITOR", "MY_APP_REGION"] {
assert!(!is_sensitive_env_name(name), "{name}");
}
}
#[test]
fn child_allowlist_is_the_short_list_not_the_environment() {
assert!(child_env_allowed("PATH"));
assert!(child_env_allowed("LANG"));
assert!(!child_env_allowed("MY_APP_REGION"));
assert!(!child_env_allowed("EDITOR"));
}
#[test]
fn script_reads_pass_unless_credential_shaped() {
let none: &[String] = &[];
assert!(script_env_allowed("PATH", none));
assert!(script_env_allowed("MY_APP_REGION", none));
assert!(!script_env_allowed("ANTHROPIC_API_KEY", none));
}
#[test]
fn allowlisting_a_credential_permits_exactly_that_one() {
let allow = vec!["MY_PROVIDER_KEY".to_string()];
assert!(script_env_allowed("MY_PROVIDER_KEY", &allow));
assert!(script_env_allowed("my_provider_key", &allow), "case");
assert!(!script_env_allowed("ANTHROPIC_API_KEY", &allow));
assert!(!script_env_allowed("ANTHROPIC_API_KEY", &["*".to_string()]));
}
}