T09 · Insecure Skill Coding Practices
- Location
index.js:4- Finding
OS Command Injection Through Untrusted Skill Input
- Content
View full analysis
Vulnerability Details
File Location:
index.js:4-8
Vulnerability Type: OS command injection
Risk Level: CriticalVulnerable Code
js async run({ input }) { const { exec } = require("child_process"); return new Promise((resolve, reject) => { exec(`hledger ${input}`, (err, stdout, stderr) => { if (err) return reject(stderr || err.message);Technical Analysis
The skill interpolates attacker-controlled
inputdirectly into a command passed to Node.jschild_process.exec(). This API executes the resulting string through a system shell. Consequently, shell metacharacters and constructs withininput, including;,&&,|, redirections, and command substitutions such as$(), are interpreted by the shell rather than treated exclusively as arguments tohledger.Prefixing the command with
hledgerdoes not restrict execution to that binary. The claim inSKILL.mdthat the skill does not permit arbitrary shell execution is inconsistent with the implementation.Attack Path
-
An attacker supplies crafted skill input containing a valid or attempted
hledgerargument followed by a shell operator and another command. -
For example, the attacker submits:
text balance; id -
The application constructs the following command:
sh hledger balance; id -
exec()invokes a shell, which treats the semicolon as a command separator. -
The shell runs both
hledger balanceandid. -
More harmful commands could then read or modify files, access process-available secrets, invoke network utilities, or alter the host within the service account's permissions.
Impact Assessment
Successful exploitation provides arbitrary command execution with the operating-system privileges of the OpenClaw/Node.js process. The accessible scope may include ledger and other files readable by that account, writable application or user data, environment variables, locally available credentials, an ...[truncated 298 chars]
-
- Remediation
View remediation
Remediation Suggestions
-
Replace
child_process.exec()withexecFile()orspawn()and ensure shell execution is disabled:js const { execFile } = require("child_process"); execFile("hledger", validatedArgs, { shell: false }, (err, stdout, stderr) => { if (err) return reject(stderr || err.message); resolve(stdout || stderr); }); -
Do not split raw input with a simple whitespace expression because that mishandles quoting and can produce ambiguous arguments. Prefer a structured input contract in which the caller supplies an argument array.
-
Validate every argument's type and length. Enforce an allowlist of supported
hledgersubcommands and options where the intended feature set permits it. -
Restrict file-related options such as
-f/--fileto explicitly authorized ledger paths. Resolve and normalize paths before comparing them against approved directories. -
Run the skill under a dedicated least-privileged account with access only to required ledger files. Limit its environment variables, filesystem permissions, and outbound network access.
-
Add regression tests using inputs containing
;,&&,|, backticks,$(), newlines, and redirection operators. Verify that these values are either rejected or passed only as literal arguments without causing secondary commands to execute. -
Correct the security documentation in
SKILL.mdso it accurately reflects the implementation and its input restrictions.
-
