T09 · Insecure Skill Coding Practices
- Location
start.js:48- Finding
Shell Command Injection in the Generated Research Bot
- Content
View full analysis
Vulnerability Details
File Location:
start.js, lines 48–55 and 94–99
Vulnerability Type: OS command injection through unsafe shell-string interpolation
Risk Level: HighThe vulnerable code is embedded in the
research-bottemplate and is written tocustom/research.jswhen a user creates a project from that template.Vulnerable search command:
js const { execSync } = require('child_process'); function searchWeb(query) { try { const output = execSync(`node ~/.openclaw/workspace/custom/free_search.js '${query}'`, { encoding: 'utf8', timeout: 30000 }); return output;Vulnerable memory command:
js // Save to memory const { execSync } = require('child_process'); try { execSync(`node ~/.openclaw/skills/openclaw-memorize/memorize.js save "research-${Date.now()}" "${topic}"`, { encoding: 'utf8' }); console.log('✅ Research saved to memory');Technical Analysis
The generated script obtains
topicfrom command-line arguments and passes it tosearchWeb:js const args = process.argv.slice(2); const topic = args.join(' ');Both vulnerable calls construct a complete shell command by interpolating this attacker-controlled value into a string passed to
execSync. BecauseexecSyncexecutes string commands through a shell, quote characters, command substitutions, and shell metacharacters in the topic can change the intended command structure.The search invocation surrounds the value with single quotes, but a supplied single quote can terminate that quoted context. The memory invocation surrounds the value with double quotes, which still permits shell substitutions on common shells. Neither location applies shell-safe escaping or passes arguments through a non-shell API.
This vulnerability exists in generated projects rather than being triggered directly by the
start.js createcommand. Once a Research Bot pr ...[truncated 1523 chars]- Remediation
View remediation
Remediation Suggestions
Replace shell command strings with a non-shell process API such as
execFileSyncorspawnSync, and supply every dynamic value as a separate argument.A hardened implementation should resemble:
js const { execFileSync } = require('child_process'); const path = require('path'); const os = require('os'); function searchWeb(query) { const scriptPath = path.join( os.homedir(), '.openclaw', 'workspace', 'custom', 'free_search.js' ); return execFileSync(process.execPath, [scriptPath, query], { encoding: 'utf8', timeout: 30000, shell: false }); }Apply the same design to the memory operation:
js const memorizePath = path.join( os.homedir(), '.openclaw', 'skills', 'openclaw-memorize', 'memorize.js' ); execFileSync( process.execPath, [memorizePath, 'save', `research-${Date.now()}`, topic], { encoding: 'utf8', shell: false } );Additional hardening measures include:
- Do not attempt to fix the issue solely with ad hoc quote replacement.
- Validate topic length and reject control characters if they are not required.
- Resolve script locations with
os.homedir()andpath.join()rather than relying on shell tilde expansion. - Use an explicit timeout for both child processes.
- Limit the environment inherited by child processes where practical.
- Add tests containing quotes, substitutions, semicolons, newlines, and other shell metacharacters to verify that they remain literal arguments.
