diff --git a/cmd/crates/soroban-test/tests/it/config.rs b/cmd/crates/soroban-test/tests/it/config.rs index 4596035c8..633416734 100644 --- a/cmd/crates/soroban-test/tests/it/config.rs +++ b/cmd/crates/soroban-test/tests/it/config.rs @@ -688,6 +688,20 @@ fn network_rm_rejects_path_traversal() { }); } +#[test] +fn contract_init_rejects_path_traversal() { + TestEnv::with_default(|sandbox| { + sandbox + .new_assert_cmd("contract") + .arg("init") + .arg("my-project") + .args(["--name", "../evil"]) + .assert() + .failure() + .stderr(predicate::str::contains("Invalid name")); + }); +} + #[test] fn contract_alias_add_rejects_path_traversal() { TestEnv::with_default(|sandbox| { diff --git a/cmd/soroban-cli/src/commands/contract/init.rs b/cmd/soroban-cli/src/commands/contract/init.rs index 6242aad6a..dd95a6b97 100644 --- a/cmd/soroban-cli/src/commands/contract/init.rs +++ b/cmd/soroban-cli/src/commands/contract/init.rs @@ -9,7 +9,7 @@ use std::{ use clap::Parser; use rust_embed::RustEmbed; -use crate::{commands::global, print}; +use crate::{commands::global, config::address::ContractName, print}; #[derive(Parser, Debug, Clone)] #[group(skip)] @@ -21,7 +21,7 @@ pub struct Cmd { default_value = "hello-world", long_help = "An optional flag to specify a new contract's name." )] - pub name: String, + pub name: ContractName, #[arg(long, long_help = "Overwrite all existing files.")] pub overwrite: bool, @@ -191,7 +191,7 @@ mod tests { let runner = Runner { args: Cmd { project_path: project_dir.to_string_lossy().to_string(), - name: "hello_world".to_string(), + name: "hello_world".parse().unwrap(), overwrite: false, }, print: print::Print::new(false), @@ -209,7 +209,7 @@ mod tests { let runner = Runner { args: Cmd { project_path: project_dir.to_string_lossy().to_string(), - name: "contract2".to_string(), + name: "contract2".parse().unwrap(), overwrite: false, }, print: print::Print::new(false), diff --git a/cmd/soroban-cli/src/config/address.rs b/cmd/soroban-cli/src/config/address.rs index 3efb4c5ba..9bd798a3e 100644 --- a/cmd/soroban-cli/src/config/address.rs +++ b/cmd/soroban-cli/src/config/address.rs @@ -218,6 +218,36 @@ impl Display for AliasName { } } +#[derive(Clone, Debug)] +pub struct ContractName(String); + +impl std::ops::Deref for ContractName { + type Target = str; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::str::FromStr for ContractName { + type Err = Error; + fn from_str(s: &str) -> Result { + validate_name(s)?; + Ok(ContractName(s.to_string())) + } +} + +impl Display for ContractName { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl AsRef for ContractName { + fn as_ref(&self) -> &std::path::Path { + std::path::Path::new(&self.0) + } +} + #[cfg(test)] mod tests { use super::*; @@ -272,4 +302,29 @@ mod tests { fn alias_name_rejects_empty() { assert!("".parse::().is_err()); } + + #[test] + fn contract_name_valid() { + assert!("hello-world".parse::().is_ok()); + assert!("my_contract_123".parse::().is_ok()); + } + + #[test] + fn contract_name_rejects_path_traversal() { + assert!("../evil".parse::().is_err()); + assert!("../../etc/passwd".parse::().is_err()); + assert!("foo/bar".parse::().is_err()); + assert!("foo\\bar".parse::().is_err()); + } + + #[test] + fn contract_name_rejects_too_long() { + assert!("a".repeat(251).parse::().is_err()); + assert!("a".repeat(250).parse::().is_ok()); + } + + #[test] + fn contract_name_rejects_empty() { + assert!("".parse::().is_err()); + } }