python: Prevent shell command injection in conda environment activation (#49160)

Xiaobo Liu created

Release Notes:

- Fixed prevent shell command injection in conda environment activation

The conda environment name was directly interpolated into the shell
command without proper escaping, which could allow command injection
if the environment name contained malicious shell metacharacters.

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>

Change summary

crates/languages/src/python.rs | 80 +++++++++++++++++++++++++++++++++++
1 file changed, 79 insertions(+), 1 deletion(-)

Detailed changes

crates/languages/src/python.rs 🔗

@@ -1393,7 +1393,15 @@ impl ToolchainLister for PythonToolchainProvider {
                     }
 
                     if let Some(name) = &toolchain.environment.name {
-                        activation_script.push(format!("{manager} activate {name}"));
+                        if let Some(quoted_name) = shell.try_quote(name) {
+                            activation_script.push(format!("{manager} activate {quoted_name}"));
+                        } else {
+                            log::warn!(
+                                "Could not safely quote environment name {:?}, falling back to base",
+                                name
+                            );
+                            activation_script.push(format!("{manager} activate base"));
+                        }
                     } else {
                         activation_script.push(format!("{manager} activate base"));
                     }
@@ -2630,6 +2638,76 @@ mod tests {
 
     use crate::python::python_module_name_from_relative_path;
 
+    #[gpui::test]
+    async fn test_conda_activation_script_injection(cx: &mut TestAppContext) {
+        use language::{LanguageName, Toolchain, ToolchainLister};
+        use settings::{CondaManager, VenvSettings};
+        use task::ShellKind;
+
+        use crate::python::PythonToolchainProvider;
+
+        cx.executor().allow_parking();
+
+        cx.update(|cx| {
+            let test_settings = SettingsStore::test(cx);
+            cx.set_global(test_settings);
+            cx.update_global::<SettingsStore, _>(|store, cx| {
+                store.update_user_settings(cx, |s| {
+                    s.terminal
+                        .get_or_insert_with(Default::default)
+                        .project
+                        .detect_venv = Some(VenvSettings::On {
+                        activate_script: None,
+                        venv_name: None,
+                        directories: None,
+                        conda_manager: Some(CondaManager::Conda),
+                    });
+                });
+            });
+        });
+
+        let provider = PythonToolchainProvider;
+        let malicious_name = "foo; rm -rf /";
+
+        let manager_executable = std::env::current_exe().unwrap();
+
+        let data = serde_json::json!({
+            "name": malicious_name,
+            "kind": "Conda",
+            "executable": "/tmp/conda/bin/python",
+            "version": serde_json::Value::Null,
+            "prefix": serde_json::Value::Null,
+            "arch": serde_json::Value::Null,
+            "displayName": serde_json::Value::Null,
+            "project": serde_json::Value::Null,
+            "symlinks": serde_json::Value::Null,
+            "manager": {
+                "executable": manager_executable,
+                "version": serde_json::Value::Null,
+                "tool": "Conda",
+            },
+        });
+
+        let toolchain = Toolchain {
+            name: "test".into(),
+            path: "/tmp/conda".into(),
+            language_name: LanguageName::new_static("Python"),
+            as_json: data,
+        };
+
+        let script = cx
+            .update(|cx| provider.activation_script(&toolchain, ShellKind::Posix, cx))
+            .await;
+
+        assert!(
+            script
+                .iter()
+                .any(|s| s.contains("conda activate 'foo; rm -rf /'")),
+            "Script should contain quoted malicious name, actual: {:?}",
+            script
+        );
+    }
+
     #[gpui::test]
     async fn test_python_autoindent(cx: &mut TestAppContext) {
         cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);