Sanitize extension environment maps (#9884)

Signed-off-by: Jasper Hugo <jasper@spiral.xyz>
This commit is contained in:
Jasper
2026-06-19 15:43:40 +02:00
committed by GitHub
parent ad64980c42
commit e6da0c6a14
3 changed files with 51 additions and 2 deletions
+21 -1
View File
@@ -58,7 +58,7 @@ pub enum ExtensionError {
pub type ExtensionResult<T> = Result<T, ExtensionError>;
#[derive(Debug, Clone, Deserialize, Serialize, Default, ToSchema, PartialEq)]
#[derive(Debug, Clone, Serialize, Default, ToSchema, PartialEq)]
pub struct Envs {
/// A map of environment variables to set, e.g. API_KEY -> some_secret, HOST -> host
#[serde(default)]
@@ -66,6 +66,16 @@ pub struct Envs {
map: HashMap<String, String>,
}
impl<'de> Deserialize<'de> for Envs {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let map = HashMap::<String, String>::deserialize(deserializer)?;
Ok(Self::new(map))
}
}
impl Envs {
/// List of sensitive env vars that should not be overridden
const DISALLOWED_KEYS: [&'static str; 31] = [
@@ -665,6 +675,16 @@ available_tools: []
}
}
#[test]
fn envs_deserialization_filters_disallowed_keys() {
let envs: extension::Envs =
serde_yaml::from_str("LD_PRELOAD: /tmp/injected.so\nSAFE_VAR: ok\n").unwrap();
let map = envs.get_env();
assert!(!map.contains_key("LD_PRELOAD"));
assert_eq!(map.get("SAFE_VAR"), Some(&"ok".to_string()));
}
#[test_case(
ExtensionConfig::Builtin {
name: "developer".into(),
+1 -1
View File
@@ -498,7 +498,7 @@ pub(crate) async fn merge_environments(
}
}
Ok(all_envs)
Ok(Envs::new(all_envs).get_env())
}
/// Substitute environment variables in a string. Supports both ${VAR} and $VAR syntax.
@@ -260,4 +260,33 @@ mod tests {
other => panic!("unexpected extension variant: {:?}", other),
}
}
#[test]
fn recipe_stdio_envs_deserialization_filters_disallowed_keys() {
let wrapper: Wrapper = serde_json::from_value(json!({
"extensions": [{
"type": "stdio",
"name": "test-stdio",
"cmd": "echo",
"args": [],
"envs": {
"LD_PRELOAD": "/tmp/injected.so",
"SAFE_VAR": "ok"
}
}]
}))
.expect("failed to deserialize extensions");
let extensions = wrapper.extensions.expect("expected extensions");
assert_eq!(extensions.len(), 1);
match &extensions[0] {
ExtensionConfig::Stdio { envs, .. } => {
let map = envs.get_env();
assert!(!map.contains_key("LD_PRELOAD"));
assert_eq!(map.get("SAFE_VAR"), Some(&"ok".to_string()));
}
other => panic!("unexpected extension variant: {:?}", other),
}
}
}