1use std::collections::HashMap;
4
5const SAFE_ENV_VARS: &[&str] = &[
6 "HOME",
7 "USER",
8 "SHELL",
9 "LANG",
10 "LC_ALL",
11 "LC_CTYPE",
12 "TERM",
13 "PATH",
14 "PWD",
15 "TMPDIR",
16 "TMP",
17 "TEMP",
18 "CARGO_HOME",
19 "RUSTUP_HOME",
20 "NODE_PATH",
21 "NPM_CONFIG_PREFIX",
22 "VIRTUAL_ENV",
23 "CONDA_PREFIX",
24 "GIT_AUTHOR_NAME",
25 "GIT_AUTHOR_EMAIL",
26 "GIT_COMMITTER_NAME",
27 "GIT_COMMITTER_EMAIL",
28 "EDITOR",
29 "VISUAL",
30 "XDG_CONFIG_HOME",
31 "XDG_DATA_HOME",
32 "XDG_CACHE_HOME",
33 "XDG_RUNTIME_DIR",
34 "DISPLAY",
35 "WAYLAND_DISPLAY",
36 "SSH_AUTH_SOCK",
37 "HTTP_PROXY",
38 "HTTPS_PROXY",
39 "NO_PROXY",
40 "ALL_PROXY",
41 "http_proxy",
42 "https_proxy",
43 "no_proxy",
44 "all_proxy",
45];
46
47const BLOCKED_ENV_PATTERNS: &[&str] = &[
48 "LD_PRELOAD",
50 "LD_LIBRARY_PATH",
51 "LD_AUDIT",
52 "LD_DEBUG",
53 "LD_PROFILE",
54 "LD_DEBUG_OUTPUT",
55 "LD_HWCAP_MASK",
56 "LD_BIND_",
57 "LD_TRACE_",
58 "DYLD_INSERT_LIBRARIES",
60 "DYLD_LIBRARY_PATH",
61 "DYLD_FRAMEWORK_PATH",
62 "DYLD_FALLBACK_",
63 "DYLD_IMAGE_",
64 "DYLD_PRINT_",
65 "CC",
67 "CXX",
68 "LD",
69 "AR",
70 "AS",
71 "CFLAGS",
72 "CXXFLAGS",
73 "LDFLAGS",
74 "CPPFLAGS",
75 "MAKEFLAGS",
76 "CMAKE_",
77 "PYTHONSTARTUP",
79 "PYTHONHOME",
80 "PYTHONUSERBASE",
81 "PYTHONWARNINGS",
82 "PYTHONEXECUTABLE",
83 "PYTHONDONTWRITEBYTECODE",
84 "BASH_ENV",
86 "ENV",
87 "BASH_FUNC_",
88 "ZDOTDIR",
89 "FPATH",
90 "CDPATH",
91 "SSH_ASKPASS",
93 "SUDO_ASKPASS",
94 "GIT_ASKPASS",
95 "GIT_SSH",
97 "GIT_SSH_COMMAND",
98 "SVN_SSH",
99 "GIT_EXEC_PATH",
100 "GIT_TEMPLATE_DIR",
101 "PROMPT_COMMAND",
103 "PS1",
104 "PS2",
105 "PS4",
106 "PERL5OPT",
108 "PERL5LIB",
109 "PERL_HASH_SEED_DEBUG",
110 "PERL_MB_OPT",
111 "PERL_MM_OPT",
112 "RUBYOPT",
113 "RUBYLIB",
114 "NODE_OPTIONS",
115 "JAVA_TOOL_OPTIONS",
116 "_JAVA_OPTIONS",
117 "JAVA_HOME",
118 "RUSTFLAGS",
120 "RUSTC_WRAPPER",
121 "RUSTC_LOG",
122 "CARGO_BUILD_",
123 "STRACE_OPTS",
125 "VALGRIND_OPTS",
126 "GDB_STARTUP_COMMANDS",
127 "LLDB_",
128 "GLIBC_TUNABLES",
130 "MALLOC_CHECK_",
131 "MALLOC_PERTURB_",
132 "IFS",
134 "LESS",
136 "LESSOPEN",
137 "LESSCLOSE",
138 "MORE",
139 "MOST",
140 "DOTNET_",
142 "POWERSHELL_",
143 "PSModulePath",
144 "GOPROXY",
146 "GOFLAGS",
147 "npm_config_",
149 "NPM_CONFIG_REGISTRY",
150 "NPM_CONFIG_CAFILE",
151 "NODE_EXTRA_CA_CERTS",
152 "YARN_",
153 "PIP_INDEX_URL",
154 "PIP_EXTRA_INDEX_URL",
155 "PIP_TRUSTED_HOST",
156 "PIPENV_",
157 "UV_INDEX_URL",
158 "UV_EXTRA_INDEX_URL",
159 "CARGO_REGISTRIES_",
160 "CARGO_NET_",
161 "BROWSER",
163 "TEXINPUTS",
164 "TERMCAP",
165 "TERMINFO",
166];
167
168const SAFE_PATH: &str = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
169
170#[derive(Debug, Clone)]
171pub struct SanitizedEnv {
172 vars: HashMap<String, String>,
173}
174
175impl SanitizedEnv {
176 pub fn from_current() -> Self {
177 Self::from_env(std::env::vars())
178 }
179
180 pub fn from_env(env: impl Iterator<Item = (String, String)>) -> Self {
181 let mut vars = HashMap::new();
182
183 for (key, value) in env {
184 if Self::is_blocked(&key) {
185 continue;
186 }
187 if Self::is_safe(&key) {
188 vars.insert(key, value);
189 }
190 }
191
192 vars.insert("PATH".to_string(), SAFE_PATH.to_string());
193
194 Self { vars }
195 }
196
197 pub fn var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
198 let key = key.into();
199 if !Self::is_blocked(&key) {
200 self.vars.insert(key, value.into());
201 }
202 self
203 }
204
205 pub fn working_dir(mut self, dir: impl AsRef<std::path::Path>) -> Self {
206 self.vars
207 .insert("PWD".to_string(), dir.as_ref().display().to_string());
208 self
209 }
210
211 pub fn vars(mut self, vars: HashMap<String, String>) -> Self {
212 for (key, value) in vars {
213 if !Self::is_blocked(&key) {
214 self.vars.insert(key, value);
215 }
216 }
217 self
218 }
219
220 pub fn get_vars(&self) -> &HashMap<String, String> {
221 &self.vars
222 }
223
224 pub fn into_vec(self) -> Vec<(String, String)> {
225 self.vars.into_iter().collect()
226 }
227
228 fn is_blocked(key: &str) -> bool {
229 BLOCKED_ENV_PATTERNS
230 .iter()
231 .any(|pattern| key.starts_with(pattern))
232 }
233
234 fn is_safe(key: &str) -> bool {
235 SAFE_ENV_VARS.contains(&key)
236 }
237}
238
239impl Default for SanitizedEnv {
240 fn default() -> Self {
241 Self::from_current()
242 }
243}
244
245impl IntoIterator for SanitizedEnv {
246 type Item = (String, String);
247 type IntoIter = std::collections::hash_map::IntoIter<String, String>;
248
249 fn into_iter(self) -> Self::IntoIter {
250 self.vars.into_iter()
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_ld_preload_blocked() {
260 let env = vec![
261 ("LD_PRELOAD".to_string(), "/evil.so".to_string()),
262 ("HOME".to_string(), "/home/user".to_string()),
263 ];
264 let sanitized = SanitizedEnv::from_env(env.into_iter());
265
266 assert!(!sanitized.vars.contains_key("LD_PRELOAD"));
267 assert!(sanitized.vars.contains_key("HOME"));
268 }
269
270 #[test]
271 fn test_bash_env_blocked() {
272 let env = vec![
273 ("BASH_ENV".to_string(), "/evil.sh".to_string()),
274 ("USER".to_string(), "test".to_string()),
275 ];
276 let sanitized = SanitizedEnv::from_env(env.into_iter());
277
278 assert!(!sanitized.vars.contains_key("BASH_ENV"));
279 assert!(sanitized.vars.contains_key("USER"));
280 }
281
282 #[test]
283 fn test_bash_func_blocked() {
284 let env = vec![(
285 "BASH_FUNC_evil%%".to_string(),
286 "() { /bin/rm -rf /; }".to_string(),
287 )];
288 let sanitized = SanitizedEnv::from_env(env.into_iter());
289
290 assert!(!sanitized.vars.contains_key("BASH_FUNC_evil%%"));
291 }
292
293 #[test]
294 fn test_safe_path_forced() {
295 let env = vec![("PATH".to_string(), "/evil:/bin".to_string())];
296 let sanitized = SanitizedEnv::from_env(env.into_iter());
297
298 assert_eq!(sanitized.vars.get("PATH").unwrap(), SAFE_PATH);
299 }
300
301 #[test]
302 fn test_working_dir() {
303 let sanitized = SanitizedEnv::from_env(std::iter::empty()).working_dir("/tmp/sandbox");
304
305 assert_eq!(sanitized.vars.get("PWD").unwrap(), "/tmp/sandbox");
306 }
307
308 #[test]
309 fn test_dyld_blocked() {
310 let env = vec![
311 (
312 "DYLD_INSERT_LIBRARIES".to_string(),
313 "/evil.dylib".to_string(),
314 ),
315 ("DYLD_LIBRARY_PATH".to_string(), "/evil/libs".to_string()),
316 ];
317 let sanitized = SanitizedEnv::from_env(env.into_iter());
318
319 assert!(!sanitized.vars.contains_key("DYLD_INSERT_LIBRARIES"));
320 assert!(!sanitized.vars.contains_key("DYLD_LIBRARY_PATH"));
321 }
322
323 #[test]
324 fn test_package_manager_blocked() {
325 let env = vec![
326 (
327 "npm_config_registry".to_string(),
328 "https://evil.com".to_string(),
329 ),
330 ("PIP_INDEX_URL".to_string(), "https://evil.com".to_string()),
331 ("YARN_REGISTRY".to_string(), "https://evil.com".to_string()),
332 (
333 "CARGO_REGISTRIES_EVIL".to_string(),
334 "https://evil.com".to_string(),
335 ),
336 ];
337 let sanitized = SanitizedEnv::from_env(env.into_iter());
338
339 assert!(!sanitized.vars.contains_key("npm_config_registry"));
340 assert!(!sanitized.vars.contains_key("PIP_INDEX_URL"));
341 assert!(!sanitized.vars.contains_key("YARN_REGISTRY"));
342 assert!(!sanitized.vars.contains_key("CARGO_REGISTRIES_EVIL"));
343 }
344}