T09 · Insecure Skill Coding Practices
- Location
tools/security-audit.js:747- Finding
Shell Command Injection Through Crafted Filenames During IOC Hashing
- Content
View full analysis
Vulnerability Details
File Location:
tools/security-audit.js, lines 747–755
Vulnerability Type: OS command injection
Risk Level: HighVulnerable Code
js const configDir = getConfigDir(); if (configDir) { const filesToCheck = findFiles(configDir, /\.(js|ts|sh|py|bin)$/); // Also inspect suspicious files in /tmp const tmpFiles = findFiles('/tmp', /^[a-z0-9]{10,}$/); const allFiles = [...filesToCheck, ...tmpFiles].slice(0, 500); // Limit scan count let hashMatchCount = 0; for (const file of allFiles) { const hash = safeExec(`shasum -a 256 "${file}" 2>/dev/null | awk '{print $1}'`);The command reaches the following shell-execution helper at lines 77–83:
js function safeExec(cmd, timeout = 5000) { try { return execSync(cmd, { timeout, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); } catch { return null; } }Technical Analysis
The recursively discovered
filepath is inserted directly into a shell command and passed toexecSync(). Wrapping the path in double quotes is not sufficient shell escaping: a filename containing a double quote, command substitution, or other shell syntax can terminate or alter the intended argument.Files beneath the OpenClaw configuration directory can originate from installed Skills. Consequently, a malicious Skill that can choose its own filenames can inject shell syntax without needing to modify the audit utility itself.
The
/tmpfilename filter only accepts lowercase alphanumeric names, which substantially limits this particular vector for/tmp; however, files discovered beneath~/.openclawor~/.clawdbotare not subject to an equivalent filename restriction.Attack Path
- An attacker publishes or installs a malicious Skill beneath the OpenClaw configuration directory.
- The Skill includes a file with a supported extension such as `.js ...[truncated 1030 chars]
- Remediation
View remediation
Remediation Suggestions
Eliminate shell interpretation from file hashing.
-
Prefer Node.js hashing APIs:
js const crypto = require('crypto'); function sha256File(filePath) { const hash = crypto.createHash('sha256'); hash.update(fs.readFileSync(filePath)); return hash.digest('hex'); }For large files, use
fs.createReadStream()and update the hash incrementally. -
If an external hashing utility is required, invoke it with an argument array rather than a command string:
js const { execFileSync } = require('child_process'); const output = execFileSync( 'shasum', ['-a', '256', file], { timeout: 5000, encoding: 'utf-8' } ); const hash = output.trim().split(/\s+/)[0]; -
Do not attempt to solve the issue solely through manual shell escaping or filename validation. Avoiding a shell is the reliable control.
-
Run the audit as an unprivileged, sandboxed account with only the read permissions needed for the selected modules.
-
Add regression tests using filenames containing spaces, quotes, dollar signs, backticks, command substitutions, and newline characters. Verify that no filename content is interpreted as executable syntax.
-
