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 "OPENROUTER_API_KEY",
23 "GROQ_API_KEY",
24 "MISTRAL_API_KEY",
25 "COHERE_API_KEY",
26 "AZURE_OPENAI_API_KEY",
27 "HUGGINGFACE_API_KEY",
28 "HF_TOKEN",
29 "AWS_ACCESS_KEY_ID",
31 "AWS_SECRET_ACCESS_KEY",
32 "AWS_SESSION_TOKEN",
33 "GOOGLE_APPLICATION_CREDENTIALS",
34 "GOOGLE_CLOUD_PROJECT",
35 "AZURE_CLIENT_ID",
36 "AZURE_CLIENT_SECRET",
37 "AZURE_TENANT_ID",
38 "AZURE_SUBSCRIPTION_ID",
39 "GITHUB_TOKEN",
41 "GH_TOKEN",
42 "GITHUB_PAT",
43 "NPM_TOKEN",
45 "NPM_AUTH_TOKEN",
46 "CARGO_REGISTRY_TOKEN",
47 "PYPI_TOKEN",
48 "DATABASE_URL",
50 "DB_PASSWORD",
51 "PGPASSWORD",
52 "MYSQL_PWD",
53 "REDIS_PASSWORD",
54 "MONGO_PASSWORD",
55 "SSH_AUTH_SOCK",
57 "GPG_AGENT_INFO",
58 "LD_PRELOAD",
60 "LD_LIBRARY_PATH",
61 "LD_AUDIT",
62 "LD_DEBUG",
63 "LD_PROFILE",
64 "DYLD_INSERT_LIBRARIES",
65 "DYLD_LIBRARY_PATH",
66 "DYLD_FRAMEWORK_PATH",
67 "DYLD_FALLBACK_LIBRARY_PATH",
68 "VAULT_TOKEN",
70 "CONSUL_HTTP_TOKEN",
71 "DOCKER_AUTH_CONFIG",
72 "KUBECONFIG",
73 "KUBE_TOKEN",
74 "SLACK_TOKEN",
75 "SLACK_BOT_TOKEN",
76 "DISCORD_TOKEN",
77 "TELEGRAM_BOT_TOKEN",
78];
79
80pub const PRESERVED_ENV_VARS: &[&str] = &[
82 "PATH",
84 "HOME",
85 "USER",
86 "SHELL",
87 "TERM",
88 "LANG",
89 "LC_ALL",
90 "LC_CTYPE",
91 "TZ",
92 "XDG_CONFIG_HOME",
94 "XDG_DATA_HOME",
95 "XDG_CACHE_HOME",
96 "XDG_RUNTIME_DIR",
97 "EDITOR",
99 "VISUAL",
100 "PAGER",
101 "CARGO_HOME",
103 "RUSTUP_HOME",
104 "GOPATH",
105 "GOROOT",
106 "JAVA_HOME",
107 "PYTHON",
108 "PYTHONPATH",
109 "NODE_PATH",
110 "COLORTERM",
112 "FORCE_COLOR",
113 "NO_COLOR",
114 "CLICOLOR",
115 "CLICOLOR_FORCE",
116 "TMPDIR",
118 "TEMP",
119 "TMP",
120];
121
122pub const VTCODE_SANDBOX_ACTIVE: &str = "VTCODE_SANDBOX_ACTIVE";
124pub const VTCODE_SANDBOX_NETWORK_DISABLED: &str = "VTCODE_SANDBOX_NETWORK_DISABLED";
125pub const VTCODE_SANDBOX_TYPE: &str = "VTCODE_SANDBOX_TYPE";
126pub const VTCODE_SANDBOX_WRITABLE_ROOTS: &str = "VTCODE_SANDBOX_WRITABLE_ROOTS";
127
128#[expect(
133 unused_results,
134 reason = "Environment construction intentionally ignores prior values while writing the sanitized snapshot."
135)]
136pub fn build_sanitized_env(
137 current_env: &HashMap<String, String>,
138 sandbox_active: bool,
139 network_disabled: bool,
140 sandbox_type: &str,
141 writable_roots: &[&Path],
142) -> HashMap<String, String> {
143 let mut sanitized = HashMap::new();
144
145 for key in PRESERVED_ENV_VARS {
147 if let Some(value) = current_env.get(*key) {
148 sanitized.insert(key.to_string(), value.clone());
149 }
150 }
151
152 if sandbox_active {
154 sanitized.insert(VTCODE_SANDBOX_ACTIVE.to_string(), "1".to_string());
155 sanitized.insert(VTCODE_SANDBOX_TYPE.to_string(), sandbox_type.to_string());
156
157 if network_disabled {
158 sanitized.insert(VTCODE_SANDBOX_NETWORK_DISABLED.to_string(), "1".to_string());
159 }
160
161 if !writable_roots.is_empty() {
162 let roots: Vec<String> = writable_roots.iter().map(|p| p.display().to_string()).collect();
163 sanitized.insert(VTCODE_SANDBOX_WRITABLE_ROOTS.to_string(), roots.join(":"));
164 }
165 }
166
167 sanitized
168}
169
170pub fn should_filter_env_var(key: &str) -> bool {
172 let key = key.to_ascii_uppercase();
173 FILTERED_ENV_VARS.contains(&key.as_str())
174 || key.starts_with("AWS_")
175 || key.starts_with("AZURE_")
176 || key.starts_with("GOOGLE_")
177 || key.starts_with("GCP_")
178 || key.starts_with("CLOUDSDK_")
179 || key.starts_with("LD_")
180 || key.starts_with("DYLD_")
181 || matches!(key.as_str(), "TOKEN" | "SECRET" | "PASSWORD" | "PASS" | "PWD" | "CREDENTIALS")
182 || key.ends_with("_TOKEN")
183 || key.ends_with("_KEY")
184 || key.ends_with("_SECRET")
185 || key.ends_with("_PASS")
186 || key.ends_with("_PWD")
187 || key.ends_with("_PASSWORD")
188 || key.ends_with("_CREDENTIALS")
189}
190
191pub fn filter_sensitive_env(env: &HashMap<String, String>) -> HashMap<String, String> {
195 env.iter()
196 .filter(|(k, _)| !should_filter_env_var(k))
197 .map(|(k, v)| (k.clone(), v.clone()))
198 .collect()
199}
200
201#[cfg(target_os = "linux")]
209pub fn setup_parent_death_signal() -> std::io::Result<()> {
210 setup_parent_death_signal_with_check(nix::unistd::getppid())
211}
212
213#[cfg(target_os = "linux")]
218pub fn setup_parent_death_signal_with_check(expected_parent_pid: nix::unistd::Pid) -> std::io::Result<()> {
219 use nix::sys::prctl;
220 use nix::sys::signal::{Signal, raise};
221 use std::io::Error;
222
223 prctl::set_pdeathsig(Some(Signal::SIGTERM))
225 .map_err(|e| Error::other(format!("prctl(PR_SET_PDEATHSIG) failed: {e}")))?;
226
227 if nix::unistd::getppid() != expected_parent_pid {
230 let _ = raise(Signal::SIGTERM);
231 }
232
233 Ok(())
234}
235
236#[cfg(not(target_os = "linux"))]
237pub fn setup_parent_death_signal() -> std::io::Result<()> {
238 Ok(())
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 const TEST_API_KEY_VALUE: &str = "test-openai-key";
246
247 #[test]
248 fn test_should_filter_sensitive_vars() {
249 assert!(should_filter_env_var("OPENAI_API_KEY"));
250 assert!(should_filter_env_var("AWS_SECRET_ACCESS_KEY"));
251 assert!(should_filter_env_var("GITHUB_TOKEN"));
252 assert!(should_filter_env_var("LD_PRELOAD"));
253 assert!(should_filter_env_var("DYLD_INSERT_LIBRARIES"));
254 assert!(should_filter_env_var("MY_CUSTOM_TOKEN"));
255 assert!(should_filter_env_var("token"));
256 assert!(should_filter_env_var("CloudSDK_AUTH_CREDENTIAL_FILE_OVERRIDE"));
257 assert!(should_filter_env_var("MY_CUSTOM_PASS"));
258 assert!(should_filter_env_var("MYSQL_PWD"));
259 assert!(should_filter_env_var("DATABASE_PASSWORD"));
260
261 assert!(!should_filter_env_var("PATH"));
262 assert!(!should_filter_env_var("HOME"));
263 assert!(!should_filter_env_var("TERM"));
264 }
265
266 #[test]
267 fn filters_sensitive_names_case_insensitively() {
268 assert!(should_filter_env_var("openai_api_key"));
269 assert!(should_filter_env_var("Aws_SECRET_ACCESS_KEY"));
270 assert!(should_filter_env_var("custom_access_token"));
271 assert!(should_filter_env_var("dyld_insert_libraries"));
272 assert!(!should_filter_env_var("PROJECT_NAME"));
273 }
274
275 #[test]
276 fn test_build_sanitized_env() {
277 let mut current = HashMap::new();
278 drop(current.insert("PATH".to_string(), "/usr/bin".to_string()));
279 drop(current.insert("HOME".to_string(), "/home/user".to_string()));
280 drop(current.insert("OPENAI_API_KEY".to_string(), TEST_API_KEY_VALUE.to_string()));
281 drop(current.insert("RANDOM_VAR".to_string(), "value".to_string()));
282
283 let sanitized = build_sanitized_env(¤t, true, true, "MacosSeatbelt", &[]);
284
285 assert_eq!(sanitized.get("PATH"), Some(&"/usr/bin".to_string()));
287 assert_eq!(sanitized.get("HOME"), Some(&"/home/user".to_string()));
288
289 assert!(!sanitized.contains_key("OPENAI_API_KEY"));
291
292 assert!(!sanitized.contains_key("RANDOM_VAR"));
294
295 assert_eq!(sanitized.get(VTCODE_SANDBOX_ACTIVE), Some(&"1".to_string()));
297 assert_eq!(sanitized.get(VTCODE_SANDBOX_NETWORK_DISABLED), Some(&"1".to_string()));
298 assert_eq!(sanitized.get(VTCODE_SANDBOX_TYPE), Some(&"MacosSeatbelt".to_string()));
299 }
300
301 #[test]
302 fn test_filter_sensitive_env() {
303 let mut env = HashMap::new();
304 drop(env.insert("PATH".to_string(), "/usr/bin".to_string()));
305 drop(env.insert("OPENAI_API_KEY".to_string(), TEST_API_KEY_VALUE.to_string()));
306 drop(env.insert("MY_VAR".to_string(), "value".to_string()));
307 drop(env.insert("AWS_ACCESS_KEY_ID".to_string(), "AKIA...".to_string()));
308 drop(env.insert("CUSTOM_PASS".to_string(), "let-me-in".to_string()));
309 drop(env.insert("SERVICE_PWD".to_string(), "super-secret".to_string()));
310
311 let filtered = filter_sensitive_env(&env);
312
313 assert!(filtered.contains_key("PATH"));
314 assert!(filtered.contains_key("MY_VAR"));
315 assert!(!filtered.contains_key("OPENAI_API_KEY"));
316 assert!(!filtered.contains_key("AWS_ACCESS_KEY_ID"));
317 assert!(!filtered.contains_key("CUSTOM_PASS"));
318 assert!(!filtered.contains_key("SERVICE_PWD"));
319 }
320}