vtcode_safety/sandboxing/
child_spawn.rs1use hashbrown::HashMap;
9use std::path::Path;
10
11pub const FILTERED_ENV_VARS: &[&str] = &[
16 "OPENAI_API_KEY",
18 "ANTHROPIC_API_KEY",
19 "GEMINI_API_KEY",
20 "XAI_API_KEY",
21 "DEEPSEEK_API_KEY",
22 "META_API_KEY",
23 "MODEL_API_KEY",
24 "OPENROUTER_API_KEY",
25 "GROQ_API_KEY",
26 "MISTRAL_API_KEY",
27 "COHERE_API_KEY",
28 "AZURE_OPENAI_API_KEY",
29 "HUGGINGFACE_API_KEY",
30 "HF_TOKEN",
31 "AWS_ACCESS_KEY_ID",
33 "AWS_SECRET_ACCESS_KEY",
34 "AWS_SESSION_TOKEN",
35 "GOOGLE_APPLICATION_CREDENTIALS",
36 "GOOGLE_CLOUD_PROJECT",
37 "AZURE_CLIENT_ID",
38 "AZURE_CLIENT_SECRET",
39 "AZURE_TENANT_ID",
40 "AZURE_SUBSCRIPTION_ID",
41 "GITHUB_TOKEN",
43 "GH_TOKEN",
44 "GITHUB_PAT",
45 "NPM_TOKEN",
47 "NPM_AUTH_TOKEN",
48 "CARGO_REGISTRY_TOKEN",
49 "PYPI_TOKEN",
50 "DATABASE_URL",
52 "DB_PASSWORD",
53 "PGPASSWORD",
54 "MYSQL_PWD",
55 "REDIS_PASSWORD",
56 "MONGO_PASSWORD",
57 "SSH_AUTH_SOCK",
59 "GPG_AGENT_INFO",
60 "LD_PRELOAD",
62 "LD_LIBRARY_PATH",
63 "LD_AUDIT",
64 "LD_DEBUG",
65 "LD_PROFILE",
66 "DYLD_INSERT_LIBRARIES",
67 "DYLD_LIBRARY_PATH",
68 "DYLD_FRAMEWORK_PATH",
69 "DYLD_FALLBACK_LIBRARY_PATH",
70 "VAULT_TOKEN",
72 "CONSUL_HTTP_TOKEN",
73 "DOCKER_AUTH_CONFIG",
74 "KUBECONFIG",
75 "KUBE_TOKEN",
76 "SLACK_TOKEN",
77 "SLACK_BOT_TOKEN",
78 "DISCORD_TOKEN",
79 "TELEGRAM_BOT_TOKEN",
80];
81
82pub const PRESERVED_ENV_VARS: &[&str] = &[
84 "PATH",
86 "HOME",
87 "USER",
88 "SHELL",
89 "TERM",
90 "LANG",
91 "LC_ALL",
92 "LC_CTYPE",
93 "TZ",
94 "XDG_CONFIG_HOME",
96 "XDG_DATA_HOME",
97 "XDG_CACHE_HOME",
98 "XDG_RUNTIME_DIR",
99 "EDITOR",
101 "VISUAL",
102 "PAGER",
103 "CARGO_HOME",
105 "RUSTUP_HOME",
106 "GOPATH",
107 "GOROOT",
108 "JAVA_HOME",
109 "PYTHON",
110 "PYTHONPATH",
111 "NODE_PATH",
112 "COLORTERM",
114 "FORCE_COLOR",
115 "NO_COLOR",
116 "CLICOLOR",
117 "CLICOLOR_FORCE",
118 "TMPDIR",
120 "TEMP",
121 "TMP",
122];
123
124pub const VTCODE_SANDBOX_ACTIVE: &str = "VTCODE_SANDBOX_ACTIVE";
126pub const VTCODE_SANDBOX_NETWORK_DISABLED: &str = "VTCODE_SANDBOX_NETWORK_DISABLED";
127pub const VTCODE_SANDBOX_TYPE: &str = "VTCODE_SANDBOX_TYPE";
128pub const VTCODE_SANDBOX_WRITABLE_ROOTS: &str = "VTCODE_SANDBOX_WRITABLE_ROOTS";
129
130#[expect(
135 unused_results,
136 reason = "Environment construction intentionally ignores prior values while writing the sanitized snapshot."
137)]
138pub fn build_sanitized_env(
139 current_env: &HashMap<String, String>,
140 sandbox_active: bool,
141 network_disabled: bool,
142 sandbox_type: &str,
143 writable_roots: &[&Path],
144) -> HashMap<String, String> {
145 let mut sanitized = HashMap::new();
146
147 for key in PRESERVED_ENV_VARS {
149 if let Some(value) = current_env.get(*key) {
150 sanitized.insert(key.to_string(), value.clone());
151 }
152 }
153
154 if sandbox_active {
156 sanitized.insert(VTCODE_SANDBOX_ACTIVE.to_string(), "1".to_string());
157 sanitized.insert(VTCODE_SANDBOX_TYPE.to_string(), sandbox_type.to_string());
158
159 if network_disabled {
160 sanitized.insert(VTCODE_SANDBOX_NETWORK_DISABLED.to_string(), "1".to_string());
161 }
162
163 if !writable_roots.is_empty() {
164 let roots: Vec<String> = writable_roots.iter().map(|p| p.display().to_string()).collect();
165 sanitized.insert(VTCODE_SANDBOX_WRITABLE_ROOTS.to_string(), roots.join(":"));
166 }
167 }
168
169 sanitized
170}
171
172pub fn should_filter_env_var(key: &str) -> bool {
174 let key = key.to_ascii_uppercase();
175 FILTERED_ENV_VARS.contains(&key.as_str())
176 || key.starts_with("AWS_")
177 || key.starts_with("AZURE_")
178 || key.starts_with("GOOGLE_")
179 || key.starts_with("GCP_")
180 || key.starts_with("CLOUDSDK_")
181 || key.starts_with("LD_")
182 || key.starts_with("DYLD_")
183 || matches!(key.as_str(), "TOKEN" | "SECRET" | "PASSWORD" | "PASS" | "PWD" | "CREDENTIALS")
184 || key.ends_with("_TOKEN")
185 || key.ends_with("_KEY")
186 || key.ends_with("_SECRET")
187 || key.ends_with("_PASS")
188 || key.ends_with("_PWD")
189 || key.ends_with("_PASSWORD")
190 || key.ends_with("_CREDENTIALS")
191}
192
193pub fn filter_sensitive_env(env: &HashMap<String, String>) -> HashMap<String, String> {
197 env.iter()
198 .filter(|(k, _)| !should_filter_env_var(k))
199 .map(|(k, v)| (k.clone(), v.clone()))
200 .collect()
201}
202
203#[cfg(target_os = "linux")]
211pub fn setup_parent_death_signal() -> std::io::Result<()> {
212 setup_parent_death_signal_with_check(nix::unistd::getppid())
213}
214
215#[cfg(target_os = "linux")]
220pub fn setup_parent_death_signal_with_check(expected_parent_pid: nix::unistd::Pid) -> std::io::Result<()> {
221 use nix::sys::prctl;
222 use nix::sys::signal::{Signal, raise};
223 use std::io::Error;
224
225 prctl::set_pdeathsig(Some(Signal::SIGTERM))
227 .map_err(|e| Error::other(format!("prctl(PR_SET_PDEATHSIG) failed: {e}")))?;
228
229 if nix::unistd::getppid() != expected_parent_pid {
232 let _ = raise(Signal::SIGTERM);
233 }
234
235 Ok(())
236}
237
238#[cfg(not(target_os = "linux"))]
239pub fn setup_parent_death_signal() -> std::io::Result<()> {
240 Ok(())
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 const TEST_API_KEY_VALUE: &str = "test-openai-key";
248
249 #[test]
250 fn test_should_filter_sensitive_vars() {
251 assert!(should_filter_env_var("OPENAI_API_KEY"));
252 assert!(should_filter_env_var("META_API_KEY"));
253 assert!(should_filter_env_var("AWS_SECRET_ACCESS_KEY"));
254 assert!(should_filter_env_var("GITHUB_TOKEN"));
255 assert!(should_filter_env_var("LD_PRELOAD"));
256 assert!(should_filter_env_var("DYLD_INSERT_LIBRARIES"));
257 assert!(should_filter_env_var("MY_CUSTOM_TOKEN"));
258 assert!(should_filter_env_var("token"));
259 assert!(should_filter_env_var("CloudSDK_AUTH_CREDENTIAL_FILE_OVERRIDE"));
260 assert!(should_filter_env_var("MY_CUSTOM_PASS"));
261 assert!(should_filter_env_var("MYSQL_PWD"));
262 assert!(should_filter_env_var("DATABASE_PASSWORD"));
263
264 assert!(!should_filter_env_var("PATH"));
265 assert!(!should_filter_env_var("HOME"));
266 assert!(!should_filter_env_var("TERM"));
267 }
268
269 #[test]
270 fn filters_sensitive_names_case_insensitively() {
271 assert!(should_filter_env_var("openai_api_key"));
272 assert!(should_filter_env_var("Aws_SECRET_ACCESS_KEY"));
273 assert!(should_filter_env_var("custom_access_token"));
274 assert!(should_filter_env_var("dyld_insert_libraries"));
275 assert!(!should_filter_env_var("PROJECT_NAME"));
276 }
277
278 #[test]
279 fn test_build_sanitized_env() {
280 let mut current = HashMap::new();
281 drop(current.insert("PATH".to_string(), "/usr/bin".to_string()));
282 drop(current.insert("HOME".to_string(), "/home/user".to_string()));
283 drop(current.insert("OPENAI_API_KEY".to_string(), TEST_API_KEY_VALUE.to_string()));
284 drop(current.insert("RANDOM_VAR".to_string(), "value".to_string()));
285
286 let sanitized = build_sanitized_env(¤t, true, true, "MacosSeatbelt", &[]);
287
288 assert_eq!(sanitized.get("PATH"), Some(&"/usr/bin".to_string()));
290 assert_eq!(sanitized.get("HOME"), Some(&"/home/user".to_string()));
291
292 assert!(!sanitized.contains_key("OPENAI_API_KEY"));
294
295 assert!(!sanitized.contains_key("RANDOM_VAR"));
297
298 assert_eq!(sanitized.get(VTCODE_SANDBOX_ACTIVE), Some(&"1".to_string()));
300 assert_eq!(sanitized.get(VTCODE_SANDBOX_NETWORK_DISABLED), Some(&"1".to_string()));
301 assert_eq!(sanitized.get(VTCODE_SANDBOX_TYPE), Some(&"MacosSeatbelt".to_string()));
302 }
303
304 #[test]
305 fn test_filter_sensitive_env() {
306 let mut env = HashMap::new();
307 drop(env.insert("PATH".to_string(), "/usr/bin".to_string()));
308 drop(env.insert("OPENAI_API_KEY".to_string(), TEST_API_KEY_VALUE.to_string()));
309 drop(env.insert("MY_VAR".to_string(), "value".to_string()));
310 drop(env.insert("AWS_ACCESS_KEY_ID".to_string(), "AKIA...".to_string()));
311 drop(env.insert("CUSTOM_PASS".to_string(), "let-me-in".to_string()));
312 drop(env.insert("SERVICE_PWD".to_string(), "super-secret".to_string()));
313
314 let filtered = filter_sensitive_env(&env);
315
316 assert!(filtered.contains_key("PATH"));
317 assert!(filtered.contains_key("MY_VAR"));
318 assert!(!filtered.contains_key("OPENAI_API_KEY"));
319 assert!(!filtered.contains_key("AWS_ACCESS_KEY_ID"));
320 assert!(!filtered.contains_key("CUSTOM_PASS"));
321 assert!(!filtered.contains_key("SERVICE_PWD"));
322 }
323
324 #[test]
325 fn filters_adversarial_credential_and_loader_names() {
326 let mut env = HashMap::new();
327 for (name, value) in [
328 ("MODEL_API_KEY", "model-secret"),
329 ("META_API_KEY", "meta-secret"),
330 ("INSTRUCTION_SECRET", "prompt-secret"),
331 ("AWS_PROFILE", "production"),
332 ("LD_AUDIT", "audit.so"),
333 ("DYLD_LIBRARY_PATH", "/tmp/injected"),
334 ("SAFE_PROJECT_NAME", "vtcode"),
335 ] {
336 drop(env.insert(name.to_string(), value.to_string()));
337 }
338
339 let filtered = filter_sensitive_env(&env);
340 let rebuilt = build_sanitized_env(&env, true, false, "test", &[]);
341
342 for sensitive in [
343 "MODEL_API_KEY",
344 "META_API_KEY",
345 "INSTRUCTION_SECRET",
346 "AWS_PROFILE",
347 "LD_AUDIT",
348 "DYLD_LIBRARY_PATH",
349 ] {
350 assert!(!filtered.contains_key(sensitive), "sensitive variable survived filtering: {sensitive}");
351 assert!(!rebuilt.contains_key(sensitive), "sensitive variable survived rebuilding: {sensitive}");
352 }
353 assert_eq!(filtered.get("SAFE_PROJECT_NAME"), Some(&"vtcode".to_string()));
354 }
355}