T09 · Insecure Skill Coding Practices
- Location
src/asset-auditor.ts:342- Finding
OS Command Injection in ClawHub Asset Audit Query
- Content
View full analysis
Vulnerability Details
File Location:
src/asset-auditor.ts:342-351
Vulnerability Type: OS command injection through shell interpolation
Risk Level: HighVulnerable Code
ts try { // Try clawhub CLI first const output = execSync(`clawhub search "${query}" --json 2>/dev/null`, { timeout: this.timeout, encoding: 'utf-8', }); const parsed = JSON.parse(output);The untrusted
queryvalue originates from the command-line argumentargs[2]and is passed without validation fromsrc/cli.ts:123-124andsrc/cli.ts:183:ts const subCmd = args[1]; // npm | github | clawhub | all const target = args[2]; // username or queryts if (subCmd === 'clawhub' || subCmd === 'all') { await auditor.auditClawHub(target); }Technical Analysis
auditClawHub()constructs a command string by interpolating attacker-controlled input and executes it throughchild_process.execSync(). BecauseexecSync()invokes a shell, placing the input inside double quotes does not make it safe. Shell constructs that remain active inside double quotes—including command substitution using$(...)or backticks—can be evaluated before theclawhubprocess starts.Shell execution is not necessary for the declared asset-audit functionality. The scanner only needs to invoke a fixed executable with a query argument. Consequently, this implementation exceeds minimum privilege by exposing a general command-execution channel instead of using an argument-vector API.
Attack Path
- An attacker supplies or persuades a user or automated workflow to use a crafted ClawHub audit query containing shell substitution syntax.
- The CLI reads the crafted query into
targetfromargs[2]. src/cli.tscallsauditor.auditClawHub(target).auditClawHub()interpolates the query into the command string.execSync()passes the string to the operating-syst ...[truncated 1303 chars]
- Remediation
View remediation
Remediation Suggestions
-
Replace shell-based
execSync()withexecFileSync()orspawnSync()and pass each argument separately:ts import { execFileSync } from 'node:child_process'; const output = execFileSync( 'clawhub', ['search', query, '--json'], { timeout: this.timeout, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], }, ); -
Do not use shell redirection such as
2>/dev/null. Capture or ignore stderr through the child-processstdiooption. -
Validate the query as defense in depth:
- Require a string.
- Enforce a reasonable maximum length.
- Reject NUL bytes and control characters.
- Apply a restrictive character allowlist if ClawHub query semantics permit it.
-
Add regression tests using metacharacters and command-substitution sequences. Verify that they are passed literally as one argument and never interpreted by a shell.
-
Run the audit process with a minimal environment and least-privileged account, especially in CI.
-
Review all other command-string construction in project scripts and migrate to argument-vector process APIs where any value may become externally controlled.
-
