Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion crates/bashkit/src/builtins/vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,37 @@ pub struct Unset;

#[async_trait]
impl Builtin for Unset {
// THREAT[TM-INJ-009]: Block unset of internal variables and readonly variables
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
let mut stderr = String::new();
let mut exit_code = 0;
for name in ctx.args {
// Block unsetting internal marker variables (_READONLY_, _NAMEREF_, etc.)
if is_internal_variable(name) {
stderr.push_str(&format!(
"bash: unset: {name}: cannot unset: readonly variable\n"
));
exit_code = 1;
continue;
}
// Block unsetting readonly variables
let readonly_key = format!("_READONLY_{name}");
if ctx.variables.contains_key(&readonly_key) {
stderr.push_str(&format!(
"bash: unset: {name}: cannot unset: readonly variable\n"
));
exit_code = 1;
continue;
}
ctx.variables.remove(name);
// Note: env is immutable in our model - environment variables
// are inherited and can't be unset by the shell
}
Ok(ExecResult::ok(String::new()))
Ok(ExecResult {
stderr,
exit_code,
..Default::default()
})
}
}

Expand Down
4 changes: 4 additions & 0 deletions crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4605,6 +4605,10 @@ impl Interpreter {
self.variables.remove(&format!("_NAMEREF_{}", arg));
} else {
let resolved = self.resolve_nameref(arg).to_string();
// THREAT[TM-INJ-009]: Block unset of internal marker variables
if is_internal_variable(&resolved) {
continue;
}
// THREAT[TM-INJ-019]: Refuse to unset readonly variables
if self
.variables
Expand Down
43 changes: 43 additions & 0 deletions crates/bashkit/tests/blackbox_security_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,49 @@ mod finding_readonly_bypass {
);
}

/// Issue #1006: unset _READONLY_* marker cannot bypass readonly protection.
#[tokio::test]
async fn unset_readonly_marker_blocked() {
let mut bash = tight_bash();
let result = bash
.exec(
r#"
readonly IMPORTANT=secret
unset _READONLY_IMPORTANT 2>/dev/null
IMPORTANT=hacked 2>/dev/null
echo "IMPORTANT=$IMPORTANT"
"#,
)
.await
.unwrap();
assert!(
result.stdout.contains("IMPORTANT=secret"),
"readonly was bypassed by unsetting _READONLY_ marker, got: {}",
result.stdout
);
}

/// Unset of normal non-readonly variables still works.
#[tokio::test]
async fn unset_normal_variable_works() {
let mut bash = tight_bash();
let result = bash
.exec(
r#"
FOO=hello
unset FOO
echo "FOO=${FOO:-empty}"
"#,
)
.await
.unwrap();
assert!(
result.stdout.contains("FOO=empty"),
"expected FOO to be unset, got: {}",
result.stdout
);
}

/// TM-INJ-020: declare cannot overwrite readonly variables.
#[tokio::test]
async fn declare_cannot_overwrite_readonly() {
Expand Down
Loading