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