fix: added path validation for skill names

This commit is contained in:
2026-06-03 14:59:57 -06:00
parent 8c5bed3e34
commit df6c3c50db
3 changed files with 57 additions and 0 deletions
+54
View File
@@ -80,6 +80,21 @@ pub fn skill_file(name: &str) -> PathBuf {
skill_dir(name).join("SKILL.md")
}
pub fn validate_skill_name(name: &str) -> Result<()> {
if name.is_empty() {
bail!("Skill name cannot be empty");
}
if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
bail!(
"Invalid skill name '{name}': only letters, digits, '-', and '_' are allowed"
);
}
Ok(())
}
pub fn macros_dir() -> PathBuf {
match env::var(get_env_name("macros_dir")) {
Ok(value) => PathBuf::from(value),
@@ -286,3 +301,42 @@ pub fn local_models_override() -> Result<Vec<ProviderModels>> {
}
Ok(models_override.list)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_skill_name_accepts_alphanumerics_and_dashes() {
assert!(validate_skill_name("git-master").is_ok());
assert!(validate_skill_name("code_review").is_ok());
assert!(validate_skill_name("Skill1").is_ok());
}
#[test]
fn validate_skill_name_rejects_empty() {
let err = validate_skill_name("").unwrap_err();
assert!(err.to_string().contains("cannot be empty"));
}
#[test]
fn validate_skill_name_rejects_path_traversal() {
for bad in ["../escape", "..", "foo/bar", "foo\\bar", "./hidden"] {
let err = validate_skill_name(bad).unwrap_err();
assert!(
err.to_string().contains("Invalid skill name"),
"expected rejection for {bad:?}, got: {err}"
);
}
}
#[test]
fn validate_skill_name_rejects_other_special_chars() {
for bad in ["with space", "null\0byte", "weird?char", "dot.name"] {
assert!(
validate_skill_name(bad).is_err(),
"expected rejection for {bad:?}"
);
}
}
}
+2
View File
@@ -2584,6 +2584,7 @@ impl RequestContext {
}
pub fn upsert_skill(&self, app: &AppConfig, name: &str) -> Result<()> {
paths::validate_skill_name(name)?;
let path = paths::skill_file(name);
ensure_parent_exists(&path)?;
let is_new = !path.exists();
@@ -2602,6 +2603,7 @@ impl RequestContext {
}
pub async fn load_skill_repl(&mut self, name: &str, abort_signal: AbortSignal) -> Result<()> {
paths::validate_skill_name(name)?;
if !self.app.config.function_calling_support {
bail!(
"Skills require function calling, which is disabled. Enable function calling in your config then try again."
+1
View File
@@ -116,6 +116,7 @@ impl Skill {
}
pub fn load(name: &str) -> Result<Self> {
paths::validate_skill_name(name)?;
let path = paths::skill_file(name);
let content = read_to_string(&path)
.with_context(|| format!("Failed to read skill '{name}' at {}", path.display()))?;