1const BLOCKED_ENV_VARS: &[&str] = &[
18 "AWS_SECRET_ACCESS_KEY",
20 "AWS_SESSION_TOKEN",
21 "GOOGLE_APPLICATION_CREDENTIALS",
22 "AZURE_CLIENT_SECRET",
23 "GITHUB_TOKEN",
25 "GH_TOKEN",
26 "GITLAB_TOKEN",
27 "NPM_TOKEN",
28 "DOCKER_PASSWORD",
30 "REGISTRY_PASSWORD",
31 "HOME",
33 "USER",
34 "LOGNAME",
35 "SHELL",
36 "HISTFILE",
37 "DATABASE_URL",
39 "REDIS_URL",
40 "MONGODB_URI",
41 "POSTGRES_PASSWORD",
42 "MYSQL_ROOT_PASSWORD",
43 "JWT_SECRET",
45 "SESSION_SECRET",
46 "ENCRYPTION_KEY",
47 "MASTER_KEY",
48 "PRIVATE_KEY",
49 "SECRET_KEY",
50];
51
52const BLOCKED_ENV_PREFIXES: &[&str] =
54 &["AWS_", "AZURE_", "GCP_", "GOOGLE_", "GITHUB_", "GITLAB_", "SSH_", "GPG_"];
55
56const BLOCKED_KEYWORDS: &[&str] =
58 &["PASSWORD", "SECRET", "TOKEN", "KEY", "CREDENTIAL", "AUTH", "PRIVATE"];
59
60#[must_use]
67pub fn is_env_var_safe(name: &str) -> bool {
68 !is_blocked(name)
69}
70
71#[must_use]
75pub fn load_safe_env_vars() -> std::collections::HashMap<String, String> {
76 std::env::vars().filter(|(name, _)| !is_blocked(name)).collect()
77}
78
79#[must_use]
83pub fn load_env_var_safe(name: &str) -> Option<String> {
84 if is_blocked(name) {
85 return None;
86 }
87 std::env::var(name).ok()
88}
89
90fn is_blocked(name: &str) -> bool {
93 if BLOCKED_ENV_VARS.iter().any(|blocked| blocked.eq_ignore_ascii_case(name)) {
95 return true;
96 }
97
98 let upper = name.to_ascii_uppercase();
100 if BLOCKED_ENV_PREFIXES.iter().any(|prefix| upper.starts_with(prefix)) {
101 return true;
102 }
103
104 if BLOCKED_KEYWORDS.iter().any(|keyword| upper.contains(keyword)) {
106 return true;
107 }
108
109 false
110}
111
112#[cfg(test)]
115#[allow(unsafe_code)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn blocks_known_sensitive_vars() {
121 assert!(!is_env_var_safe("AWS_SECRET_ACCESS_KEY"));
122 assert!(!is_env_var_safe("GITHUB_TOKEN"));
123 assert!(!is_env_var_safe("DATABASE_URL"));
124 assert!(!is_env_var_safe("HOME"));
125 assert!(!is_env_var_safe("SSH_AUTH_SOCK"));
126 }
127
128 #[test]
129 fn blocks_prefix_match() {
130 assert!(!is_env_var_safe("AWS_REGION"));
131 assert!(!is_env_var_safe("AZURE_SUBSCRIPTION_ID"));
132 assert!(!is_env_var_safe("GCP_PROJECT"));
133 assert!(!is_env_var_safe("GOOGLE_CLOUD_KEY"));
134 }
135
136 #[test]
137 fn blocks_keyword_match() {
138 assert!(!is_env_var_safe("MY_PASSWORD"));
139 assert!(!is_env_var_safe("API_SECRET"));
140 assert!(!is_env_var_safe("ACCESS_TOKEN"));
141 assert!(!is_env_var_safe("ENCRYPTION_KEY"));
142 assert!(!is_env_var_safe("CUSTOM_CREDENTIAL"));
143 }
144
145 #[test]
146 fn allows_safe_vars() {
147 assert!(is_env_var_safe("RUST_LOG"));
148 assert!(is_env_var_safe("CARGO_HOME"));
149 assert!(is_env_var_safe("LANG"));
150 assert!(is_env_var_safe("TERM"));
151 assert!(is_env_var_safe("BOB_MODEL"));
152 }
153
154 #[test]
155 fn case_insensitive() {
156 assert!(!is_env_var_safe("github_token"));
157 assert!(!is_env_var_safe("Database_Url"));
158 assert!(!is_env_var_safe("aws_secret_access_key"));
159 }
160
161 #[test]
163 fn load_safe_env_vars_filters() {
164 unsafe {
167 std::env::set_var("BOB_TEST_SAFE_VAR", "safe_value");
168 std::env::set_var("BOB_TEST_PASSWORD", "secret_value");
169 }
170
171 let vars = load_safe_env_vars();
172 assert!(vars.contains_key("BOB_TEST_SAFE_VAR"));
173 assert!(!vars.contains_key("BOB_TEST_PASSWORD"));
174
175 unsafe {
177 std::env::remove_var("BOB_TEST_SAFE_VAR");
178 std::env::remove_var("BOB_TEST_PASSWORD");
179 }
180 }
181
182 #[test]
183 fn load_env_var_safe_blocks_sensitive() {
184 unsafe { std::env::set_var("MY_API_TOKEN", "secret") };
186 assert!(load_env_var_safe("MY_API_TOKEN").is_none());
187 unsafe { std::env::remove_var("MY_API_TOKEN") };
189 }
190
191 #[test]
192 fn load_env_var_safe_returns_safe() {
193 unsafe { std::env::set_var("BOB_TEST_LOADED", "value") };
195 assert_eq!(load_env_var_safe("BOB_TEST_LOADED"), Some("value".to_string()));
196 unsafe { std::env::remove_var("BOB_TEST_LOADED") };
198 }
199}