Skip to content

Commit ee0b605

Browse files
committed
fix(interpreter): block internal variable namespace injection
THREAT[TM-INJ-009]: User scripts could assign to internal marker prefixes (_NAMEREF_, _READONLY_, _UPPER_, _LOWER_) to manipulate interpreter behavior. Block user assignment in set_variable() and filter internal markers from ${!prefix*} expansion. Closes #407 https://claude.ai/code/session_01WZjYqxm5xMPAEe7FSHJkDy
1 parent aa19823 commit ee0b605

File tree

1 file changed

+48
-1
lines changed
  • crates/bashkit/src/interpreter

1 file changed

+48
-1
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6037,11 +6037,17 @@ impl Interpreter {
60376037
.variables
60386038
.keys()
60396039
.filter(|k| k.starts_with(prefix.as_str()))
6040+
// THREAT[TM-INJ-009]: Hide internal marker variables
6041+
.filter(|k| !Self::is_internal_variable(k))
60406042
.cloned()
60416043
.collect();
60426044
// Also check env
60436045
for k in self.env.keys() {
6044-
if k.starts_with(prefix.as_str()) && !names.contains(k) {
6046+
if k.starts_with(prefix.as_str())
6047+
&& !names.contains(k)
6048+
// THREAT[TM-INJ-009]: Hide internal marker variables
6049+
&& !Self::is_internal_variable(k)
6050+
{
60456051
names.push(k.clone());
60466052
}
60476053
}
@@ -7519,10 +7525,24 @@ impl Interpreter {
75197525
s.to_string()
75207526
}
75217527

7528+
/// THREAT[TM-INJ-009]: Check if a variable name is an internal marker.
7529+
fn is_internal_variable(name: &str) -> bool {
7530+
name.starts_with("_NAMEREF_")
7531+
|| name.starts_with("_READONLY_")
7532+
|| name.starts_with("_UPPER_")
7533+
|| name.starts_with("_LOWER_")
7534+
|| name == "_SHIFT_COUNT"
7535+
|| name == "_SET_POSITIONAL"
7536+
}
7537+
75227538
/// Set a variable, respecting dynamic scoping.
75237539
/// If the variable is declared `local` in any active call frame, update that frame.
75247540
/// Otherwise, set in global variables.
75257541
fn set_variable(&mut self, name: String, value: String) {
7542+
// THREAT[TM-INJ-009]: Block user assignment to internal marker variables
7543+
if Self::is_internal_variable(&name) {
7544+
return;
7545+
}
75267546
// Resolve nameref: if `name` is a nameref, assign to the target instead
75277547
let resolved = self.resolve_nameref(&name).to_string();
75287548
// Apply case conversion attributes (declare -l / declare -u)
@@ -9374,4 +9394,31 @@ mod tests {
93749394
let result = run_script("echo $(( 9223372036854775807 * 2 ))").await;
93759395
assert_eq!(result.exit_code, 0);
93769396
}
9397+
9398+
#[tokio::test]
9399+
async fn test_internal_var_prefix_not_exposed() {
9400+
// ${!_NAMEREF*} must not expose internal markers
9401+
let result = run_script("echo \"${!_NAMEREF*}\"").await;
9402+
assert_eq!(result.stdout.trim(), "");
9403+
}
9404+
9405+
#[tokio::test]
9406+
async fn test_internal_var_readonly_not_exposed() {
9407+
let result = run_script("echo \"${!_READONLY*}\"").await;
9408+
assert_eq!(result.stdout.trim(), "");
9409+
}
9410+
9411+
#[tokio::test]
9412+
async fn test_internal_var_assignment_blocked() {
9413+
// Direct assignment to _NAMEREF_ prefix should be silently ignored
9414+
let result = run_script("_NAMEREF_x=PATH; echo ${!x}").await;
9415+
assert!(!result.stdout.contains("/usr"));
9416+
}
9417+
9418+
#[tokio::test]
9419+
async fn test_internal_var_readonly_injection_blocked() {
9420+
// Should not be able to fake readonly
9421+
let result = run_script("_READONLY_myvar=1; myvar=hello; echo $myvar").await;
9422+
assert_eq!(result.stdout.trim(), "hello");
9423+
}
93779424
}

0 commit comments

Comments
 (0)