sqlite_graphrag/spawn/
env_whitelist.rs1use std::process::Command;
29
30pub const PRESERVED_ENV_VARS: &[&str] = &[
35 "PATH",
37 "HOME",
38 "USER",
39 "SHELL",
40 "TERM",
41 "LANG",
42 "XDG_CONFIG_HOME",
43 "XDG_DATA_HOME",
44 "XDG_RUNTIME_DIR",
45 "XDG_CACHE_HOME",
46 "TMPDIR",
48 "TMP",
49 "TEMP",
50 "DYLD_FALLBACK_LIBRARY_PATH",
52 "CLAUDE_CONFIG_DIR",
54 "ANTHROPIC_AUTH_TOKEN",
56 "ANTHROPIC_BASE_URL",
57 "CLAUDE_CODE_ENTRYPOINT",
58 "CODEX_ACCESS_TOKEN",
60 "OPENAI_BASE_URL",
61 ];
64
65#[cfg(windows)]
67pub const PRESERVED_ENV_VARS_WINDOWS: &[&str] = &[
68 "LOCALAPPDATA",
69 "APPDATA",
70 "USERPROFILE",
71 "SystemRoot",
72 "COMSPEC",
73 "PATHEXT",
74 "HOMEPATH",
75 "HOMEDRIVE",
76];
77
78pub fn apply_env_whitelist(cmd: &mut Command, strict: bool) {
83 cmd.env_clear();
84 if strict {
85 if let Ok(path) = std::env::var("PATH") {
86 cmd.env("PATH", path);
87 }
88 return;
89 }
90 for var in PRESERVED_ENV_VARS {
91 if let Ok(val) = std::env::var(var) {
92 cmd.env(var, val);
93 }
94 }
95 #[cfg(windows)]
96 for var in PRESERVED_ENV_VARS_WINDOWS {
97 if let Ok(val) = std::env::var(var) {
98 cmd.env(var, val);
99 }
100 }
101}
102
103pub fn is_strict_env_clear() -> bool {
108 if crate::runtime_config::get().strict_env_clear {
109 return true;
110 }
111 crate::config::get_setting("spawn.strict_env_clear")
112 .ok()
113 .flatten()
114 .is_some_and(|v| {
115 matches!(
116 v.trim().to_ascii_lowercase().as_str(),
117 "1" | "true" | "yes" | "on"
118 )
119 })
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 fn captured_env(cmd: &Command) -> Vec<(String, String)> {
128 cmd.get_envs()
129 .filter_map(|(k, v)| {
130 let k = k.to_str()?.to_string();
131 let v = v?.to_str()?.to_string();
132 Some((k, v))
133 })
134 .collect()
135 }
136
137 #[test]
138 #[serial_test::serial(env)]
139 fn whitelist_includes_custom_provider_vars() {
140 unsafe {
142 std::env::set_var("ANTHROPIC_AUTH_TOKEN", "sk-cp-test");
143 std::env::set_var("ANTHROPIC_BASE_URL", "https://api.minimax.io/anthropic");
144 std::env::set_var("OPENAI_BASE_URL", "https://api.openrouter.ai/v1");
145 }
146 let mut cmd = std::process::Command::new("/usr/bin/false");
147 apply_env_whitelist(&mut cmd, false);
148 let envs = captured_env(&cmd);
149 let has_token = envs
150 .iter()
151 .any(|(k, v)| k == "ANTHROPIC_AUTH_TOKEN" && v == "sk-cp-test");
152 let has_anthropic_url = envs
153 .iter()
154 .any(|(k, v)| k == "ANTHROPIC_BASE_URL" && v == "https://api.minimax.io/anthropic");
155 let has_openai_url = envs
156 .iter()
157 .any(|(k, v)| k == "OPENAI_BASE_URL" && v == "https://api.openrouter.ai/v1");
158 unsafe {
159 std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
160 std::env::remove_var("ANTHROPIC_BASE_URL");
161 std::env::remove_var("OPENAI_BASE_URL");
162 }
163 assert!(has_token, "ANTHROPIC_AUTH_TOKEN not preserved");
164 assert!(has_anthropic_url, "ANTHROPIC_BASE_URL not preserved");
165 assert!(has_openai_url, "OPENAI_BASE_URL not preserved");
166 }
167
168 #[test]
169 #[serial_test::serial(env)]
170 fn whitelist_excludes_api_key_vars() {
171 unsafe {
173 std::env::set_var("ANTHROPIC_API_KEY", "sk-ant-violation");
174 std::env::set_var("OPENAI_API_KEY", "sk-violation");
175 }
176 let mut cmd = std::process::Command::new("/usr/bin/false");
177 apply_env_whitelist(&mut cmd, false);
178 let envs = captured_env(&cmd);
179 let has_anthropic_key = envs.iter().any(|(k, _)| k == "ANTHROPIC_API_KEY");
180 let has_openai_key = envs.iter().any(|(k, _)| k == "OPENAI_API_KEY");
181 unsafe {
182 std::env::remove_var("ANTHROPIC_API_KEY");
183 std::env::remove_var("OPENAI_API_KEY");
184 }
185 assert!(
186 !has_anthropic_key,
187 "ANTHROPIC_API_KEY must NEVER reach subprocess"
188 );
189 assert!(
190 !has_openai_key,
191 "OPENAI_API_KEY must NEVER reach subprocess"
192 );
193 }
194
195 #[test]
196 #[serial_test::serial(env)]
197 fn strict_mode_drops_credentials() {
198 unsafe {
200 std::env::set_var("ANTHROPIC_AUTH_TOKEN", "sk-cp-strict-test");
201 std::env::set_var("PATH", "/usr/bin:/bin");
202 }
203 let mut cmd = std::process::Command::new("/usr/bin/false");
204 apply_env_whitelist(&mut cmd, true);
205 let envs = captured_env(&cmd);
206 let has_token = envs.iter().any(|(k, _)| k == "ANTHROPIC_AUTH_TOKEN");
207 let has_path = envs
208 .iter()
209 .any(|(k, v)| k == "PATH" && v == "/usr/bin:/bin");
210 unsafe {
211 std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
212 }
213 assert!(!has_token, "strict mode must drop credentials");
214 assert!(has_path, "strict mode preserves PATH only");
215 }
216}