mirror of
https://github.com/openai/codex.git
synced 2026-09-13 11:47:17 +00:00
## 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
107 lines
3.0 KiB
Rust
107 lines
3.0 KiB
Rust
//! Serialize prepared snapshot values without inspecting credentials or executing shell code.
|
|
//! Native declarations stay unchanged unless the credential stage supplies a replacement.
|
|
|
|
use super::capture::CapturedSnapshot;
|
|
|
|
impl CapturedSnapshot<'_> {
|
|
/// Render native functions, options, and aliases; the executor restores environment separately.
|
|
pub fn render_state(&self) -> String {
|
|
format!("{}{}", self.state, self.aliases)
|
|
}
|
|
|
|
/// Render a complete replay script without applying credential or environment policy.
|
|
pub fn render_script(&self) -> String {
|
|
let mut script = self.render_state();
|
|
script.push_str("# exports (native declarations)\n");
|
|
for export in &self.exports {
|
|
script.push_str(&export.source);
|
|
}
|
|
script
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub(super) struct Value {
|
|
pub parts: Vec<ValuePart>,
|
|
}
|
|
|
|
pub(super) enum ValuePart {
|
|
Literal(String),
|
|
Credential { key: String },
|
|
}
|
|
|
|
pub(super) enum Export<'a> {
|
|
Captured(&'a str),
|
|
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 {
|
|
fn render(&self) -> Option<String> {
|
|
let mut output = String::new();
|
|
for part in &self.parts {
|
|
match part {
|
|
ValuePart::Literal(value) => {
|
|
output.push_str(shlex::try_quote(value).ok()?.as_ref())
|
|
}
|
|
ValuePart::Credential { key } => {
|
|
output.push_str(&format!("\"${{{key}-}}\""));
|
|
}
|
|
}
|
|
}
|
|
if output.is_empty() {
|
|
output.push_str("''");
|
|
}
|
|
Some(output)
|
|
}
|
|
}
|
|
|
|
pub(super) fn render(state: &str, aliases: &str, exports: &[Export<'_>]) -> Option<String> {
|
|
let mut output = format!("{state}{aliases}");
|
|
if !exports.is_empty() {
|
|
output.push_str("# exports (native declarations)\n");
|
|
}
|
|
for export in exports {
|
|
match export {
|
|
Export::Captured(source) => output.push_str(source),
|
|
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)
|
|
}
|