Skip to main content

claude_agent/security/bash/
env.rs

1//! Environment variable sanitization.
2
3use 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    // Dynamic linker injection
49    "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    // macOS dynamic linker
59    "DYLD_INSERT_LIBRARIES",
60    "DYLD_LIBRARY_PATH",
61    "DYLD_FRAMEWORK_PATH",
62    "DYLD_FALLBACK_",
63    "DYLD_IMAGE_",
64    "DYLD_PRINT_",
65    // Compiler/build tool injection
66    "CC",
67    "CXX",
68    "LD",
69    "AR",
70    "AS",
71    "CFLAGS",
72    "CXXFLAGS",
73    "LDFLAGS",
74    "CPPFLAGS",
75    "MAKEFLAGS",
76    "CMAKE_",
77    // Python injection
78    "PYTHONSTARTUP",
79    "PYTHONHOME",
80    "PYTHONUSERBASE",
81    "PYTHONWARNINGS",
82    "PYTHONEXECUTABLE",
83    "PYTHONDONTWRITEBYTECODE",
84    // Shell startup injection
85    "BASH_ENV",
86    "ENV",
87    "BASH_FUNC_",
88    "ZDOTDIR",
89    "FPATH",
90    "CDPATH",
91    // Password/auth prompts
92    "SSH_ASKPASS",
93    "SUDO_ASKPASS",
94    "GIT_ASKPASS",
95    // SSH/Git command override
96    "GIT_SSH",
97    "GIT_SSH_COMMAND",
98    "SVN_SSH",
99    "GIT_EXEC_PATH",
100    "GIT_TEMPLATE_DIR",
101    // Shell prompt commands
102    "PROMPT_COMMAND",
103    "PS1",
104    "PS2",
105    "PS4",
106    // Language runtime injection
107    "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    // Rust injection
119    "RUSTFLAGS",
120    "RUSTC_WRAPPER",
121    "RUSTC_LOG",
122    "CARGO_BUILD_",
123    // Debugger/tracing injection
124    "STRACE_OPTS",
125    "VALGRIND_OPTS",
126    "GDB_STARTUP_COMMANDS",
127    "LLDB_",
128    // glibc exploits
129    "GLIBC_TUNABLES",
130    "MALLOC_CHECK_",
131    "MALLOC_PERTURB_",
132    // IFS (field separator attacks)
133    "IFS",
134    // Pager exploits (less/more can execute commands)
135    "LESS",
136    "LESSOPEN",
137    "LESSCLOSE",
138    "MORE",
139    "MOST",
140    // .NET/PowerShell injection
141    "DOTNET_",
142    "POWERSHELL_",
143    "PSModulePath",
144    // Go injection
145    "GOPROXY",
146    "GOFLAGS",
147    // Package manager injection
148    "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    // Misc dangerous
162    "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}