Skip to content

fix: replace exec() with execFile() in status command to prevent command injection - #762

Closed
thebenignhacker wants to merge 3 commits into
NVIDIA:mainfrom
opena2a-upstream:fix/C-001-command-injection-status
Closed

thebenignhacker wants to merge 3 commits into
NVIDIA:mainfrom
opena2a-upstream:fix/C-001-command-injection-status

Conversation

@thebenignhacker

@thebenignhacker thebenignhacker commented Mar 24, 2026 •

Copy link
Copy Markdown

Summary

sandboxStatus() passes the sandbox name through run() which uses
spawnSync("bash", ["-c", cmd]) for command execution. While shellQuote()
and validateName() provide defense-in-depth, the proper fix eliminates
shell interpretation entirely by using spawnSync with an argument array.

Change

Replace run() bash -c call with direct spawnSync("openshell", [...args])
in sandboxStatus. This removes shell interpretation from the code path
that handles sandbox names.

Detection

This vulnerability class is detectable via HackMyAgent:

npx hackmyagent secure .

References

  • PSIRT disclosure: tickets 6009892–6010011
  • CWE-78: Improper Neutralization of Special Elements used in an OS Command

Summary by CodeRabbit

  • Bug Fixes

    • Safer execution of the sandbox status command to prevent shell metacharacters from affecting the host and improve stability when checking sandbox state.
  • Tests

    • Added regression and end-to-end tests ensuring the status command handles malicious input safely and produces no unintended side effects.

…dboxStatus

The sandboxStatus function passed the sandbox name through `run()` which
uses `bash -c` for command execution. While shellQuote() and validateName()
provide defense-in-depth, the proper fix eliminates shell interpretation
entirely by using spawnSync with an argument array.

Detection: npx hackmyagent secure .

References:
- PSIRT disclosure: tickets 6009892-6010011
- CWE-78: Improper Neutralization of Special Elements used in an OS Command
@coderabbitai

coderabbitai Bot commented Mar 24, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Replaced a shell-invoking execution in sandboxStatus() with a direct spawnSync("openshell", ["sandbox","get", sandboxName], ...) call to avoid shell interpretation. Added two regression tests: a static source assertion and an end-to-end canary test validating that sandbox names with shell metacharacters cannot create files.

Changes

Cohort / File(s) Summary
Sandbox CLI change
bin/nemoclaw.js
Replaced shell-based run(...) usage with spawnSync("openshell", ["sandbox","get", sandboxName], ...); removed shell redirections and external error-suppression; adjusted stdio and cwd handling.
Regression tests
test/runner.test.js
Added two tests: a static source-inspection asserting no shell: true, no run* helpers, and that spawnSync("openshell", [...]) is used with sandboxName as a discrete arg; and an end-to-end canary test that runs node bin/nemoclaw.js status with a sandbox name containing shell metacharacters and verifies the command fails and no canary file is created (includes cleanup).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I nibble code where semicolons hide,
I swapped the shell for a safer stride,
Two tests on watch, the canary’s denied,
My whiskers twitch — secure and dignified. 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main security-focused change: replacing exec() with execFile() to prevent command injection in the status command.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/runner.test.js`:
- Around line 204-213: The test currently passes without exercising
sandboxStatus(); update the test to guarantee the successful sandbox lookup and
invocation of sandboxStatus by seeding a registry/fixture or stubbing the lookup
to return a valid sandbox before calling the CLI so the code path reaches
sandboxStatus(), then tighten the static assertions to catch indirect usages
(e.g., detect template-literal interpolation even when assigned to a variable
like const cmd or passed via run(cmd)) by asserting that sandboxStatus's body
uses spawnSync("openshell", ...) and does not call run(...) with a template
interpolation anywhere in the function; do the same for the related test at the
other location to ensure the e2e case is backed by a registry fixture or a
narrower seam that provably hits the status action.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8f510470-cd02-4387-88e1-6845fb3916ef

📥 Commits

Reviewing files that changed from the base of the PR and between 166319d and 7d502af.

📒 Files selected for processing (2)
  • bin/nemoclaw.js
  • test/runner.test.js

Comment thread test/runner.test.js Resolved
Broaden the static analysis assertions to detect bash -c, shell: true,
and any run/runCapture/runInteractive variant — not just template-literal
interpolation.

Addresses CodeRabbit review feedback.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
test/runner.test.js (1)

217-235: ⚠️ Potential issue | 🟠 Major

This mostly re-tests validateName(), not sandboxStatus().

Lines 123-129 already prove names containing ; are rejected, so the payload on Line 223 can fail before the status path executes. That leaves Lines 230-231 able to pass without touching the new spawnSync("openshell", ...) path. Seed a known sandbox/registry fixture or stub the lookup so this case definitely enters sandboxStatus() first.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/runner.test.js` around lines 217 - 235, The test "sandboxStatus rejects
shell metacharacters in sandbox name (e2e)" is re-testing validateName() instead
of exercising sandboxStatus() because the malformed name is rejected earlier;
update the test so the payload reaches sandboxStatus() by seeding or stubbing
the lookup that resolves sandbox names (e.g., stub the registry lookup or
provide a known sandbox fixture) and ensure the spawnSync call that uses
openshell/`sandboxStatus()` path is invoked (refer to the test name, spawnSync
invocation, and the sandboxStatus/validateName functions) so the assertion
verifies sandboxStatus() rejects shell metacharacters rather than failing at
validateName().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@test/runner.test.js`:
- Around line 217-235: The test "sandboxStatus rejects shell metacharacters in
sandbox name (e2e)" is re-testing validateName() instead of exercising
sandboxStatus() because the malformed name is rejected earlier; update the test
so the payload reaches sandboxStatus() by seeding or stubbing the lookup that
resolves sandbox names (e.g., stub the registry lookup or provide a known
sandbox fixture) and ensure the spawnSync call that uses
openshell/`sandboxStatus()` path is invoked (refer to the test name, spawnSync
invocation, and the sandboxStatus/validateName functions) so the assertion
verifies sandboxStatus() rejects shell metacharacters rather than failing at
validateName().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f4aab967-119d-438b-b64e-d71059c38764

📥 Commits

Reviewing files that changed from the base of the PR and between 7d502af and 7482aef.

📒 Files selected for processing (1)
  • test/runner.test.js

@wscurran

Copy link
Copy Markdown
Contributor

Thanks for submitting this PR, it improves the security of the status command by replacing exec() with execFile(), which could prevent command injection attacks and improve the overall security of the NemoClaw project.

- Register a sandbox in the registry before the e2e test so the CLI
  reaches sandboxStatus() instead of exiting at the lookup
- Assert spawnSync args include sandboxName as a discrete array element
- Clean up registry entry in finally block

Addresses CodeRabbit review feedback.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/runner.test.js`:
- Around line 218-246: The test uses a fixed fakeName and a loose status check
which can cause collisions and false positives; update the test to generate a
unique sandbox name (e.g., append a timestamp/random suffix to the fakeName used
with registerSandbox and removeSandbox) so it cannot collide with existing
registry state, and strengthen the spawnSync assertion by first asserting the
command did not time out or error (e.g., ensure result.status is not null and/or
result.error is undefined) before asserting result.status !== 0 to avoid passing
on a timeout; keep the canary creation/removal and removeSandbox cleanup as-is
but reference registerSandbox, removeSandbox, fakeName and the spawnSync result
when making these changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 98c6d783-5695-4a5c-ad8d-90aab42ebe68

📥 Commits

Reviewing files that changed from the base of the PR and between 7482aef and d474fe6.

📒 Files selected for processing (1)
  • test/runner.test.js

Comment thread test/runner.test.js
Comment on lines +218 to +246
it("sandboxStatus rejects shell metacharacters in sandbox name (e2e)", () => {
// Seed a registry entry so the CLI reaches sandboxStatus()
// (without a matching sandbox, the CLI exits at the lookup before our code path)
const registryPath = path.join(import.meta.dirname, "..", "bin", "lib", "registry");
const { registerSandbox, removeSandbox } = require(registryPath);
const fakeName = "canary-test-sb";
registerSandbox(fakeName, { model: "test", provider: "test" });

const canaryDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-status-canary-"));
const canary = path.join(canaryDir, "executed");
try {
// Even with a valid sandbox, metacharacters in argv must not execute
const result = spawnSync("node", [
path.join(import.meta.dirname, "..", "bin", "nemoclaw.js"),
`${fakeName}; touch ${canary}`,
"status",
], {
encoding: "utf-8",
timeout: 10000,
cwd: path.join(import.meta.dirname, ".."),
});
expect(result.status).not.toBe(0);
expect(fs.existsSync(canary)).toBe(false);
} finally {
fs.rmSync(canaryDir, { recursive: true, force: true });
removeSandbox(fakeName);
}
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Harden this e2e to avoid false positives and fixture collisions.

Two reliability gaps: (1) fixed fakeName can collide with existing registry state, and (2) expect(result.status).not.toBe(0) can still pass on timeout (status === null). Use a unique sandbox name and assert no timeout/error occurred.

🔧 Suggested patch
-      const fakeName = "canary-test-sb";
-      registerSandbox(fakeName, { model: "test", provider: "test" });
-
-      const canaryDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-status-canary-"));
-      const canary = path.join(canaryDir, "executed");
-      try {
+      const fakeName = `canary-test-sb-${process.pid}-${Date.now()}`;
+      let canaryDir;
+      try {
+        registerSandbox(fakeName, { model: "test", provider: "test" });
+        canaryDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-status-canary-"));
+        const canary = path.join(canaryDir, "executed");
         // Even with a valid sandbox, metacharacters in argv must not execute
         const result = spawnSync("node", [
           path.join(import.meta.dirname, "..", "bin", "nemoclaw.js"),
           `${fakeName}; touch ${canary}`,
           "status",
         ], {
           encoding: "utf-8",
           timeout: 10000,
           cwd: path.join(import.meta.dirname, ".."),
         });
+        expect(result.error).toBeUndefined();
+        expect(result.signal).toBeNull();
         expect(result.status).not.toBe(0);
         expect(fs.existsSync(canary)).toBe(false);
       } finally {
-        fs.rmSync(canaryDir, { recursive: true, force: true });
+        if (canaryDir) fs.rmSync(canaryDir, { recursive: true, force: true });
         removeSandbox(fakeName);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/runner.test.js` around lines 218 - 246, The test uses a fixed fakeName
and a loose status check which can cause collisions and false positives; update
the test to generate a unique sandbox name (e.g., append a timestamp/random
suffix to the fakeName used with registerSandbox and removeSandbox) so it cannot
collide with existing registry state, and strengthen the spawnSync assertion by
first asserting the command did not time out or error (e.g., ensure
result.status is not null and/or result.error is undefined) before asserting
result.status !== 0 to avoid passing on a timeout; keep the canary
creation/removal and removeSandbox cleanup as-is but reference registerSandbox,
removeSandbox, fakeName and the spawnSync result when making these changes.

@drobison00 drobison00 self-assigned this Mar 24, 2026
@thebenignhacker

Copy link
Copy Markdown
Author

Hi — these PRs are not automated. They are companion fixes to a coordinated disclosure filed with NVIDIA PSIRT (tickets 6009892–6010011, 10 findings total).

The other 4 PRs (#763, #764, #765, #766) were closed as "automated" or "duplicate," but they address distinct vulnerabilities in different files:

If it's easier to review, I'm happy to consolidate all 5 fixes into this single PR. Let me know how you'd prefer to proceed.

@wscurran

Copy link
Copy Markdown
Contributor

Thanks for identifying this and for the thorough writeup — CWE-78 with PSIRT references is exactly the kind of security contribution we want to see.

This was addressed comprehensively in #584 (fix(security): command injection across all CLI entry points), which replaced all execSync() calls with execFileSync() across every CLI entry point, added shellQuote() and validateName() utilities, and fixed path traversal in loadPreset(). That landed on main as of March 21.

Closing as superseded, but your work here helped validate that the fix covered the right surface.

@wscurran wscurran closed this Apr 14, 2026
@wscurran wscurran added bug-fix PR fixes a bug or regression and removed bug labels Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug-fix PR fixes a bug or regression

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants