diff --git a/src/cortex-cli/src/plugin_cmd.rs b/src/cortex-cli/src/plugin_cmd.rs index 3ce99f23..937b476a 100644 --- a/src/cortex-cli/src/plugin_cmd.rs +++ b/src/cortex-cli/src/plugin_cmd.rs @@ -16,7 +16,7 @@ use anyhow::{Context, Result, bail}; use clap::Parser; use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use serde::Serialize; -use std::path::{Path, PathBuf}; +use std::path::{Component, Path, PathBuf}; use std::process::{Command, Stdio}; use std::sync::mpsc; use std::time::Duration; @@ -703,6 +703,22 @@ fn get_plugins_dir() -> PathBuf { .unwrap_or_else(|| PathBuf::from(".cortex/plugins")) } +fn installed_plugin_path(plugins_dir: &Path, name: &str) -> Result { + let mut components = Path::new(name).components(); + let is_single_component = matches!(components.next(), Some(Component::Normal(_))) + && components.next().is_none() + && !name.chars().any(|c| matches!(c, '/' | '\\' | '\0')); + + if name.is_empty() || !is_single_component { + bail!( + "Invalid plugin name '{}'. Use an installed plugin name, not a path.", + name + ); + } + + Ok(plugins_dir.join(name)) +} + // ============================================================================= // Plugin Scaffolding Functions // ============================================================================= @@ -1145,7 +1161,7 @@ fn copy_dir_recursive(src: &std::path::Path, dst: &std::path::Path) -> Result<() async fn run_remove(args: PluginRemoveArgs) -> Result<()> { let plugins_dir = get_plugins_dir(); - let plugin_path = plugins_dir.join(&args.name); + let plugin_path = installed_plugin_path(&plugins_dir, &args.name)?; if !plugin_path.exists() { bail!("Plugin '{}' is not installed.", args.name); @@ -1171,7 +1187,7 @@ async fn run_remove(args: PluginRemoveArgs) -> Result<()> { async fn run_enable(args: PluginEnableArgs) -> Result<()> { let plugins_dir = get_plugins_dir(); - let plugin_path = plugins_dir.join(&args.name); + let plugin_path = installed_plugin_path(&plugins_dir, &args.name)?; let manifest_path = plugin_path.join("plugin.toml"); if !manifest_path.exists() { @@ -1192,7 +1208,7 @@ async fn run_enable(args: PluginEnableArgs) -> Result<()> { async fn run_disable(args: PluginDisableArgs) -> Result<()> { let plugins_dir = get_plugins_dir(); - let plugin_path = plugins_dir.join(&args.name); + let plugin_path = installed_plugin_path(&plugins_dir, &args.name)?; let manifest_path = plugin_path.join("plugin.toml"); if !manifest_path.exists() { @@ -1213,7 +1229,7 @@ async fn run_disable(args: PluginDisableArgs) -> Result<()> { async fn run_show(args: PluginShowArgs) -> Result<()> { let plugins_dir = get_plugins_dir(); - let plugin_path = plugins_dir.join(&args.name); + let plugin_path = installed_plugin_path(&plugins_dir, &args.name)?; let manifest_path = plugin_path.join("plugin.toml"); if !manifest_path.exists() { diff --git a/src/cortex-cli/tests/plugin_remove_path.rs b/src/cortex-cli/tests/plugin_remove_path.rs new file mode 100644 index 00000000..b3f92506 --- /dev/null +++ b/src/cortex-cli/tests/plugin_remove_path.rs @@ -0,0 +1,29 @@ +use std::process::Command; + +use tempfile::tempdir; + +#[test] +fn plugin_remove_rejects_absolute_path() { + let home_dir = tempdir().unwrap(); + let outside_dir = tempdir().unwrap(); + let target = outside_dir.path().join("srcplugin"); + std::fs::create_dir_all(&target).unwrap(); + std::fs::write(target.join("plugin.toml"), "name = \"demo\"\n").unwrap(); + std::fs::write(target.join("marker.txt"), "keep\n").unwrap(); + + let output = Command::new(env!("CARGO_BIN_EXE_Cortex")) + .args(["plugin", "remove", "-y", target.to_str().unwrap()]) + .env("HOME", home_dir.path()) + .env_remove("CORTEX_HOME") + .output() + .unwrap(); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(!output.status.success()); + assert!( + stderr.contains("Invalid plugin name"), + "unexpected stderr: {stderr}" + ); + assert!(target.exists(), "absolute plugin path was deleted"); + assert!(target.join("marker.txt").exists()); +}