Skip to content

Commit 209e630

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 6703421 commit 209e630

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
@@ -6066,11 +6066,17 @@ impl Interpreter {
60666066
.variables
60676067
.keys()
60686068
.filter(|k| k.starts_with(prefix.as_str()))
6069+
// THREAT[TM-INJ-009]: Hide internal marker variables
6070+
.filter(|k| !Self::is_internal_variable(k))
60696071
.cloned()
60706072
.collect();
60716073
// Also check env
60726074
for k in self.env.keys() {
6073-
if k.starts_with(prefix.as_str()) && !names.contains(k) {
6075+
if k.starts_with(prefix.as_str())
6076+
&& !names.contains(k)
6077+
// THREAT[TM-INJ-009]: Hide internal marker variables
6078+
&& !Self::is_internal_variable(k)
6079+
{
60746080
names.push(k.clone());
60756081
}
60766082
}
@@ -7548,10 +7554,24 @@ impl Interpreter {
75487554
s.to_string()
75497555
}
75507556

7557+
/// THREAT[TM-INJ-009]: Check if a variable name is an internal marker.
7558+
fn is_internal_variable(name: &str) -> bool {
7559+
name.starts_with("_NAMEREF_")
7560+
|| name.starts_with("_READONLY_")
7561+
|| name.starts_with("_UPPER_")
7562+
|| name.starts_with("_LOWER_")
7563+
|| name == "_SHIFT_COUNT"
7564+
|| name == "_SET_POSITIONAL"
7565+
}
7566+
75517567
/// Set a variable, respecting dynamic scoping.
75527568
/// If the variable is declared `local` in any active call frame, update that frame.
75537569
/// Otherwise, set in global variables.
75547570
fn set_variable(&mut self, name: String, value: String) {
7571+
// THREAT[TM-INJ-009]: Block user assignment to internal marker variables
7572+
if Self::is_internal_variable(&name) {
7573+
return;
7574+
}
75557575
// Resolve nameref: if `name` is a nameref, assign to the target instead
75567576
let resolved = self.resolve_nameref(&name).to_string();
75577577
// Apply case conversion attributes (declare -l / declare -u)
@@ -9436,4 +9456,31 @@ mod tests {
94369456
assert_eq!(result.exit_code, 0);
94379457
assert_eq!(result.stdout.trim(), "sourced");
94389458
}
9459+
9460+
#[tokio::test]
9461+
async fn test_internal_var_prefix_not_exposed() {
9462+
// ${!_NAMEREF*} must not expose internal markers
9463+
let result = run_script("echo \"${!_NAMEREF*}\"").await;
9464+
assert_eq!(result.stdout.trim(), "");
9465+
}
9466+
9467+
#[tokio::test]
9468+
async fn test_internal_var_readonly_not_exposed() {
9469+
let result = run_script("echo \"${!_READONLY*}\"").await;
9470+
assert_eq!(result.stdout.trim(), "");
9471+
}
9472+
9473+
#[tokio::test]
9474+
async fn test_internal_var_assignment_blocked() {
9475+
// Direct assignment to _NAMEREF_ prefix should be silently ignored
9476+
let result = run_script("_NAMEREF_x=PATH; echo ${!x}").await;
9477+
assert!(!result.stdout.contains("/usr"));
9478+
}
9479+
9480+
#[tokio::test]
9481+
async fn test_internal_var_readonly_injection_blocked() {
9482+
// Should not be able to fake readonly
9483+
let result = run_script("_READONLY_myvar=1; myvar=hello; echo $myvar").await;
9484+
assert_eq!(result.stdout.trim(), "hello");
9485+
}
94399486
}

0 commit comments

Comments
 (0)