diff --git a/crates/bashkit/src/builtins/export.rs b/crates/bashkit/src/builtins/export.rs index 76de2be3..60d2e6dd 100644 --- a/crates/bashkit/src/builtins/export.rs +++ b/crates/bashkit/src/builtins/export.rs @@ -37,10 +37,9 @@ impl Builtin for Export { let value = &arg[eq_pos + 1..]; // Validate variable name if !is_valid_var_name(name) { - return Ok(ExecResult::err( - format!("export: `{}': not a valid identifier\n", arg), - 1, - )); + stderr.push_str(&format!("export: `{arg}': not a valid identifier\n")); + exit_code = 1; + continue; } // THREAT[TM-INJ-015]: Block internal variable prefix injection via export if is_internal_variable(name) { diff --git a/crates/bashkit/tests/blackbox_security_tests.rs b/crates/bashkit/tests/blackbox_security_tests.rs index e70e27fc..e7cb493a 100644 --- a/crates/bashkit/tests/blackbox_security_tests.rs +++ b/crates/bashkit/tests/blackbox_security_tests.rs @@ -474,6 +474,27 @@ mod finding_readonly_bypass { assert!(result.stdout.contains("ok")); } + /// export with invalid identifier must continue processing later valid operands. + #[tokio::test] + async fn export_continues_after_invalid_identifier_and_exports_good_args() { + let mut bash = tight_bash(); + let result = bash + .exec( + r#" + GOOD=old + export 1BAD=x GOOD=new + echo "exit=$?" + echo "var=$GOOD" + printenv GOOD + "#, + ) + .await + .unwrap(); + assert!(result.stdout.contains("exit=1")); + assert!(result.stdout.contains("var=new")); + assert!(result.stdout.contains("new")); + } + /// Non-finding: readonly via local in function is bash-compatible shadowing. #[tokio::test] async fn local_shadows_readonly_in_function() {