From 74dff2da3db8714814f8bed165656a9b901a505d Mon Sep 17 00:00:00 2001 From: alex-clawd Date: Tue, 19 May 2026 15:55:43 -0700 Subject: [PATCH] fix: tighten @org/name ref validation to prevent path traversal Reject refs with multiple slashes (@org/a/b), dot segments (@../skill), or leading dots in org/name. Applied to both CLI install() and SDK parse_registry_ref() so the contract is enforced consistently. --- lib/cli/src/crewai_cli/skills/main.py | 16 ++++++++++++---- lib/crewai/src/crewai/skills/registry.py | 16 ++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/lib/cli/src/crewai_cli/skills/main.py b/lib/cli/src/crewai_cli/skills/main.py index 24b4c9e25..84f4b1b4b 100644 --- a/lib/cli/src/crewai_cli/skills/main.py +++ b/lib/cli/src/crewai_cli/skills/main.py @@ -92,16 +92,24 @@ class SkillCommand(BaseCommand, PlusAPIMixin): raise SystemExit(1) without_at = ref[1:] - if "/" not in without_at: + if without_at.count("/") != 1: console.print( "[red]Invalid skill reference. Use the format @org/name.[/red]" ) raise SystemExit(1) - org, _, name = without_at.partition("/") - if not org or not name: + org, name = without_at.split("/", 1) + if ( + not org + or not name + or org.startswith(".") + or name.startswith(".") + or len(Path(org).parts) != 1 + or len(Path(name).parts) != 1 + ): console.print( - "[red]Invalid skill reference: org and name must be non-empty.[/red]" + "[red]Invalid skill reference: org and name must be single, " + "non-empty path segments (no slashes, no '..').[/red]" ) raise SystemExit(1) diff --git a/lib/crewai/src/crewai/skills/registry.py b/lib/crewai/src/crewai/skills/registry.py index 965243ab7..5024e0ec2 100644 --- a/lib/crewai/src/crewai/skills/registry.py +++ b/lib/crewai/src/crewai/skills/registry.py @@ -48,14 +48,22 @@ def parse_registry_ref(ref: str) -> tuple[str, str]: if not ref.startswith("@"): raise ValueError(f"Registry reference must start with '@', got: {ref!r}") without_at = ref[1:] - if "/" not in without_at: + if without_at.count("/") != 1: raise ValueError( f"Registry reference must be in '@org/name' format, got: {ref!r}" ) - org, _, name = without_at.partition("/") - if not org or not name: + org, name = without_at.split("/", 1) + if ( + not org + or not name + or org.startswith(".") + or name.startswith(".") + or "/" in org + or "/" in name + ): raise ValueError( - f"Registry reference must have non-empty org and name, got: {ref!r}" + f"Registry reference org and name must be single, non-empty path " + f"segments (no '..' or leading dots), got: {ref!r}" ) return org, name