Harden credential handling in shell snapshots and replay (#44040)

## Why

Shell quoting can hide credentials from raw-text checks, and startup files can restore real credentials after the broker replaces them with dummy values.

## What changed

- Decode shell literals without evaluating them and reject snapshots containing credentials in executable source, including aliases, functions, and heredocs.
- Preserve credential policy overrides, explicit unsets, and aliases whose source variables were removed. Support credential aliases in Zsh tied arrays while rejecting credentials that span array elements.
- Guard snapshot replay against credential restoration through shell startup files and preserve unrelated `ENV` settings.
- Apply Windows environment-key casing rules to credential overrides, suppress unredacted sandbox diagnostics during snapshot capture, and clear inherited environment variables before launching escalated commands.

## Testing

Add regression coverage for shell quoting and escaped credentials, Zsh tied arrays, startup-file replay, readonly credentials, policy overrides, and sensitive capture timeout and cancellation handling.

GitOrigin-RevId: 58274c07c715423241d26ce6dd2c2b4230cf0f64
This commit is contained in:
Winston Howes
2026-09-09 04:00:10 +00:00
committed by copyberry
parent a548463b78
commit ec512d2347
21 changed files with 4215 additions and 256 deletions
@@ -32,8 +32,20 @@ pub(super) enum ValuePart {
pub(super) enum Export<'a> {
Captured(&'a str),
Alias { key: &'a str, value: Value },
Assignment { declaration: &'a str, value: Value },
Assignment {
declaration: &'a str,
value: Value,
},
Array {
prefix: &'a str,
elements: Vec<Value>,
suffix: &'a str,
},
ArrayBinding {
key: &'a str,
declaration: &'a str,
suffix: &'a str,
},
}
impl Value {
@@ -64,12 +76,30 @@ pub(super) fn render(state: &str, aliases: &str, exports: &[Export<'_>]) -> Opti
for export in exports {
match export {
Export::Captured(source) => output.push_str(source),
Export::Alias { key, value } => {
output.push_str(&format!("export {key}={}\n", value.render()?));
}
Export::Assignment { declaration, value } => {
output.push_str(&format!("{declaration}={}\n", value.render()?));
}
Export::Array {
prefix,
elements,
suffix,
} => {
let elements = elements
.iter()
.map(Value::render)
.collect::<Option<Vec<_>>>()?;
output.push_str(&format!("{prefix}({}){suffix}", elements.join(" ")));
}
Export::ArrayBinding {
key,
declaration,
suffix,
} => {
output.push_str(&format!(
"if [ \"${{{key}+x}}\" = x ]; then\n{declaration}{}\nfi\n",
suffix.trim_end()
));
}
}
}
Some(output)