T09 · Insecure Skill Coding Practices
- Location
scripts/setup.cjs:24- Finding
Shell Command Injection Through OPENCLAW_HOME
- Content
View full analysis
Vulnerability Details
File Location:
scripts/setup.cjs:14, 24-28, 67-72
Vulnerability Type: OS command injection caused by unsafe shell command construction
Risk Level: HighVulnerable Code
The workspace root is derived from the externally controlled
OPENCLAW_HOMEenvironment variable:js const OPENCLAW_DIR = process.env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw'); const TEAM_DIR = path.join(OPENCLAW_DIR, TEAM_NAME);Commands are converted into a single string and interpreted by a system shell:
js function runCommand(args) { try { return { code: 0, output: execSync(args.join(' '), { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], shell: true }) }; } catch (e) { return { code: e.status || 1, output: e.stderr || e.stdout || e.message }; } }The attacker-influenced workspace path is manually placed inside double quotes and passed to the vulnerable command runner:
js const { code, output } = runCommand([ 'openclaw', 'agents', 'add', agent.id, '--name', `"${agent.name}"`, '--workspace', `"${path.join(TEAM_DIR, agent.id)}"`, '--description', `"${agent.desc}"`, ]);Technical Analysis
OPENCLAW_HOMEis process-environment input and is not validated or safely escaped. Its value propagates throughOPENCLAW_DIR,TEAM_DIR, andpath.join()into the--workspacecommand argument.runCommand()joins the argument array with spaces and invokesexecSync()withshell: true. Consequently, the array does not provide argument-boundary protection: the resulting string is parsed by the operating-system shell.Manually surrounding the workspace path with double quotes does not prevent shell injection. Shell command substitution constructs such as
$(command)and backtick substitutions remain active inside double-quoted strings. An environment value containing such syntax can therefore cause an arbitrary command to e ...[truncated 1889 chars]- Remediation
View remediation
Remediation Suggestions
- Replace shell-based execution with an API that invokes a fixed executable and passes arguments separately. For example, use
execFileSync()orspawnSync()withshell: false.
js const { execFileSync } = require('child_process'); function runOpenClaw(args) { try { return { code: 0, output: execFileSync('openclaw', args, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], shell: false, }), }; } catch (e) { return { code: e.status || 1, output: e.stderr || e.stdout || e.message, }; } }- Pass raw arguments without manually adding quotation marks:
js runOpenClaw([ 'agents', 'add', agent.id, '--name', agent.name, '--workspace', path.join(TEAM_DIR, agent.id), '--description', agent.desc, ]);-
Apply the same non-shell execution pattern to every OpenClaw invocation, including version checks and agent listing.
-
Validate
OPENCLAW_HOMEas a filesystem path. Reject null bytes and unexpected control characters, resolve it withpath.resolve(), and optionally require it to fall under an explicitly permitted base directory. Validation should supplement, not replace, removal of shell interpretation. -
Do not attempt to fix the issue using ad hoc shell escaping. Correct argument separation with
shell: falseis the primary security control. -
Add regression tests using workspace paths containing spaces, quotes, dollar signs, backticks, semicolons, and command-substitution syntax. Verify that these values are treated only as literal path data and never interpreted by a shell.
- Replace shell-based execution with an API that invokes a fixed executable and passes arguments separately. For example, use
