From 8dc621d8ab3d1d006c58238b1f8e38f1558f042c Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Thu, 11 Jun 2026 20:28:23 -0500 Subject: [PATCH] fix(python): isolate unexported shell variables --- crates/bashkit/src/builtins/python.rs | 17 ++++------------- .../tests/integration/python_security_tests.rs | 10 ++-------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/crates/bashkit/src/builtins/python.rs b/crates/bashkit/src/builtins/python.rs index 3c0af82f2..5b5c94807 100644 --- a/crates/bashkit/src/builtins/python.rs +++ b/crates/bashkit/src/builtins/python.rs @@ -433,24 +433,15 @@ impl Builtin for Python { )); } - // Merge env and variables so exported vars (set via `export`) are visible - // to Python's os.getenv(). Variables override env (bash semantics). - // THREAT[TM-INF]: Filter internal markers (including SHOPT_*) to prevent - // information disclosure via os.environ (issue #999). - let mut merged_env = ctx.env.clone(); - merged_env.extend( - ctx.variables - .iter() - .filter(|(k, _)| !crate::interpreter::is_hidden_variable(k)) - .map(|(k, v)| (k.clone(), v.clone())), - ); - + // THREAT[TM-INF]: Python environment access is intentionally scoped + // to exported variables only (`ctx.env`). Shell-local variables in + // `ctx.variables` may contain wrapper secrets or internal markers. run_python( &code, &filename, ctx.fs.clone(), ctx.cwd, - &merged_env, + ctx.env, &self.limits, self.external_fns.as_ref(), ) diff --git a/crates/bashkit/tests/integration/python_security_tests.rs b/crates/bashkit/tests/integration/python_security_tests.rs index af9991297..f11f4e2b8 100644 --- a/crates/bashkit/tests/integration/python_security_tests.rs +++ b/crates/bashkit/tests/integration/python_security_tests.rs @@ -582,14 +582,8 @@ mod whitebox_env_security { .exec("INTERNAL_VAR=secret\npython3 -c \"import os\nprint(os.getenv('INTERNAL_VAR', 'none'))\"") .await .unwrap(); - // Unexported vars should not be visible to Python - // (bash semantics: only exported vars are in env) - // Note: bashkit merges variables, so this tests that behavior - if r.exit_code == 0 { - // If visible, verify it's the expected value (no corruption) - let out = r.stdout.trim(); - assert!(out == "none" || out == "secret"); - } + assert_eq!(r.exit_code, 0); + assert_eq!(r.stdout.trim(), "none"); } #[tokio::test]