T09 · Insecure Skill Coding Practices
- Location
scripts/publish_skill.py:21- Finding
Shell Command Injection Through User-Controlled CLI Arguments
- Content
View full analysis
Vulnerability Details
File Location:
scripts/publish_skill.py, lines 21-29, 145-147, and 162-167
Vulnerability Type: OS command injection through unsafe shell command construction
Risk Level: CriticalVulnerable Code
python def run_cmd(cmd, cwd=None): """执行 shell 命令""" print(f"🔧 执行:{cmd}") result = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True) if result.stdout: print(result.stdout.strip()) if result.returncode != 0 and result.stderr: print(f"❌ 错误:{result.stderr.strip()}") return result.returncode == 0, result.stdout, result.stderrpython # 提交 commit_msg = f"v{version}: {message}" success, _, _ = run_cmd(f'git commit -m "{commit_msg}"', cwd=git_cwd)python def publish_to_clawhub(cwd, slug, version, changelog): """发布到 ClawHub""" print(f"🚀 发布到 ClawHub: {slug}@{version}") cmd = f"npx clawhub@latest publish {cwd} --slug {slug} --version {version} --changelog \"{changelog}\"" success, output, stderr = run_cmd(cmd)Technical Analysis
The
run_cmdfunction invokessubprocess.runwithshell=True. The--changelogand--slugcommand-line arguments, as well as the resolved Skill path, are interpolated directly into shell command strings.The changelog is enclosed in double quotes, but this is not adequate shell escaping. An attacker can include a double quote followed by shell operators, command substitutions, redirects, or separators. The same changelog value reaches both the Git commit command and the ClawHub publication command.
The slug and path are also not validated or safely encoded. Although a path must identify an existing directory, directory names can contain shell metacharacters.
Attack Path
- An attacker convinces a user or automated workflow to publish a Skill using an attacker-controlled changelog, slug, or directory path.
- The attacker ...[truncated 1267 chars]
- Remediation
View remediation
Remediation Suggestions
-
Remove
shell=Trueand represent every command as an argument list:python def run_cmd(args, cwd=None): return subprocess.run( args, shell=False, cwd=cwd, capture_output=True, text=True, check=False, ) -
Invoke Git without constructing a shell command:
python commit_msg = f"v{version}: {message}" run_cmd(["git", "commit", "-m", commit_msg], cwd=git_cwd) -
Invoke ClawHub with separate arguments:
python run_cmd([ "npx", "clawhub@PINNED_VERSION", "publish", cwd, "--slug", slug, "--version", version, "--changelog", changelog, ]) -
Validate slugs against an explicit allowlist pattern, such as lowercase letters, digits, and hyphens.
-
Validate versions using a fully anchored semantic-version pattern.
-
Resolve and validate filesystem paths before use.
-
Add regression tests using quotes, semicolons, command substitutions, newlines, and leading option characters in every user-controlled argument.
-
