From 31765d561dff8f1935968c473c9b477bf932be3d Mon Sep 17 00:00:00 2001 From: anh nguyen <29374105+aprprprr@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:44:11 -0700 Subject: [PATCH] fix: validate sandbox and instance names to prevent shell injection Sandbox names from user input are interpolated into shell commands without sanitization. A malicious name with shell metacharacters could execute arbitrary commands. Add validateName() in runner.js that enforces [a-zA-Z0-9._-]{1,63} and call it at all entry points: CLI dispatch, deploy, and onboard. Signed-off-by: Anh Nguyen <29374105+aprprprr@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 Signed-off-by: anh nguyen <29374105+aprprprr@users.noreply.github.com> --- bin/lib/onboard.js | 3 ++- bin/lib/runner.js | 15 ++++++++++++++- bin/nemoclaw.js | 4 +++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index 2070408b5..b2b75797f 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -5,7 +5,7 @@ const fs = require("fs"); const path = require("path"); -const { ROOT, SCRIPTS, run, runCapture } = require("./runner"); +const { ROOT, SCRIPTS, run, runCapture, validateName } = require("./runner"); const { prompt, ensureApiKey, getCredential } = require("./credentials"); const registry = require("./registry"); const nim = require("./nim"); @@ -125,6 +125,7 @@ async function createSandbox(gpu) { const nameAnswer = await prompt(" Sandbox name [my-assistant]: "); const sandboxName = nameAnswer || "my-assistant"; + validateName(sandboxName); // Check if sandbox already exists in registry const existing = registry.getSandbox(sandboxName); diff --git a/bin/lib/runner.js b/bin/lib/runner.js index 57bccf6a6..77302b6c2 100644 --- a/bin/lib/runner.js +++ b/bin/lib/runner.js @@ -45,4 +45,17 @@ function runCapture(cmd, opts = {}) { } } -module.exports = { ROOT, SCRIPTS, run, runCapture }; +/** + * Validate a sandbox or instance name to prevent shell injection. + * Names must be 1–63 chars, alphanumeric with hyphens and underscores, + * and must start with a letter or digit. + */ +function validateName(name) { + if (!name || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$/.test(name)) { + console.error(` Invalid name: '${String(name).slice(0, 40)}'`); + console.error(" Names must be 1–63 characters: letters, digits, hyphens, underscores, dots."); + process.exit(1); + } +} + +module.exports = { ROOT, SCRIPTS, run, runCapture, validateName }; diff --git a/bin/nemoclaw.js b/bin/nemoclaw.js index a8a31188a..1b14ec219 100755 --- a/bin/nemoclaw.js +++ b/bin/nemoclaw.js @@ -7,7 +7,7 @@ const path = require("path"); const fs = require("fs"); const os = require("os"); -const { ROOT, SCRIPTS, run, runCapture } = require("./lib/runner"); +const { ROOT, SCRIPTS, run, runCapture, validateName } = require("./lib/runner"); const { ensureApiKey, ensureGithubToken, @@ -57,6 +57,7 @@ async function deploy(instanceName) { console.error(" nemoclaw deploy nemoclaw-test"); process.exit(1); } + validateName(instanceName); await ensureApiKey(); if (isRepoPrivate("NVIDIA/OpenShell")) { await ensureGithubToken(); @@ -329,6 +330,7 @@ const [cmd, ...args] = process.argv.slice(2); } // Sandbox-scoped commands: nemoclaw + validateName(cmd); const sandbox = registry.getSandbox(cmd); if (sandbox) { const action = args[0] || "connect";