T09 · Insecure Skill Coding Practices
- Location
monitor.js:150- Finding
Shell Command Injection Through Unvalidated Session IDs
- Content
View full analysis
Vulnerability Details
File Location:
monitor.js, lines 150-153
Vulnerability Type: OS command injection
Risk Level: HighVulnerable Code
js for (const session of sessions) { log('INFO', `Killing zombie session: ${session.id}`); const result = runCommand(`openclaw sessions kill ${session.id}`, 5000);The
session.idvalue originates from JSON returned by:js const result = runCommand('openclaw sessions list --json', 10000); const sessions = JSON.parse(result.output);Technical Analysis
The Skill builds a shell command by directly interpolating
session.idinto a string passed torunCommand(). That function invokes the command throughchild_process.execSync(), which executes it using a shell:js function runCommand(cmd, timeout = 10000) { try { const result = execSync(cmd, { encoding: 'utf8', timeout, stdio: ['pipe', 'pipe', 'pipe'] });No identifier validation, argument escaping, or shell-free process invocation is applied. Consequently, shell metacharacters embedded in a session identifier can terminate or alter the intended command and introduce additional commands.
Although the session list is obtained from the local OpenClaw CLI, it must not be treated as inherently safe. Exploitation is possible if an attacker can influence stored session metadata, the CLI output, or another component responsible for creating session identifiers.
Attack Path
- An attacker causes
openclaw sessions list --jsonto return a session whoseidcontains shell metacharacters and an injected command. - The attacker ensures that the session has a
busystatus and alastActivityvalue older than the configured 30-minute threshold. checkAgentSessions()classifies the crafted session as a zombie session.healthCheck()passes the session tofixZombieSessions().- The crafted identifier is interpolat ...[truncated 954 chars]
- An attacker causes
- Remediation
View remediation
Remediation Suggestions
Replace shell-based command execution with a shell-free API and pass each argument separately:
js const { execFileSync } = require('child_process'); function runOpenClaw(args, timeout = 10000) { try { const output = execFileSync('openclaw', args, { encoding: 'utf8', timeout, stdio: ['ignore', 'pipe', 'pipe'], shell: false }); return { success: true, output: output.trim() }; } catch (error) { return { success: false, error: error.message, output: error.stdout?.trim() || '' }; } } const sessionId = String(session.id); const result = runOpenClaw(['sessions', 'kill', sessionId], 5000);In addition:
- Validate session IDs against the exact identifier format documented by OpenClaw, such as a strict UUID validator where applicable.
- Reject identifiers containing whitespace, control characters, shell metacharacters, or unexpected Unicode characters.
- Apply length limits before logging or using identifiers.
- Use separate argument arrays for every OpenClaw invocation rather than accepting general shell command strings.
- Run the monitor as a dedicated, unprivileged account with access limited to the necessary OpenClaw resources.
- Add tests containing hostile identifiers to verify that they are rejected or passed only as literal arguments.
