From 2dd832a8139cab5881ad870cd8ccbdb5b37e5a0b Mon Sep 17 00:00:00 2001 From: Jasper Hugo Date: Fri, 19 Jun 2026 10:27:14 +0200 Subject: [PATCH] Sanitize extension environment maps Filter disallowed extension environment variables when Envs is deserialized, and apply the same filtering after merging configured environment keys. Add regression coverage for direct Envs deserialization and recipe-provided stdio extension env maps. Signed-off-by: Jasper Hugo --- crates/goose/src/agents/extension.rs | 22 +++++++++++++- crates/goose/src/agents/extension_manager.rs | 2 +- .../src/recipe/recipe_extension_adapter.rs | 29 +++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index 8ffa0f8c6ede..2d4a5377cf25 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -58,7 +58,7 @@ pub enum ExtensionError { pub type ExtensionResult = Result; -#[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, } +impl<'de> Deserialize<'de> for Envs { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let map = HashMap::::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(), diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index a8ad771318b3..995a32488ee5 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -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. diff --git a/crates/goose/src/recipe/recipe_extension_adapter.rs b/crates/goose/src/recipe/recipe_extension_adapter.rs index 2d88c7aa3099..0372f4a5d966 100644 --- a/crates/goose/src/recipe/recipe_extension_adapter.rs +++ b/crates/goose/src/recipe/recipe_extension_adapter.rs @@ -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), + } + } }