Skip to content

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

Open
thebenignhacker wants to merge 2 commits intoNVIDIA:mainfrom
opena2a-org:fix/C-001-command-injection-status
Open

fix: replace exec() with execFile() in status command to prevent command injection#762
thebenignhacker wants to merge 2 commits intoNVIDIA:mainfrom
opena2a-org:fix/C-001-command-injection-status

Conversation

@thebenignhacker
Copy link

@thebenignhacker thebenignhacker commented Mar 24, 2026

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

    • Improved stability and safer execution of the sandbox status command to prevent unsafe input from affecting host state.
  • Tests

    • Added regression and end-to-end tests validating sandbox status behavior and ensuring malicious input does not produce 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
Copy link

coderabbitai bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

Replaced shell-based execution in sandboxStatus() with a direct spawnSync("openshell", [...]) invocation to avoid shell interpretation. Added two regression tests: a static source check enforcing the new call pattern and an end-to-end canary test ensuring inputs with shell metacharacters cannot trigger commands or create files.

Changes

Cohort / File(s) Summary
Sandbox CLI change
bin/nemoclaw.js
Replaced shell-invoking run(...) usage with spawnSync("openshell", ["sandbox","get", sandboxName], ...); removed shell redirections and external error-suppression patterns; adjusted stdio and cwd handling.
Regression tests
test/runner.test.js
Added two tests: a static source inspection asserting spawnSync("openshell", [...]) without shell: true or run* calls, and an end-to-end canary test that passes a sandbox name with shell metacharacters and verifies the command fails and no canary file is created.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hopped through code with whiskers twitching bright,
Replaced the shell with a safer light,
Tests sniff the canary, guard day and night,
No stray semicolons will give me a fright — hooray! ✨

🚥 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 accurately describes the main security fix: replacing exec() with execFile() in the status command to prevent command injection. This directly aligns with the core change in sandboxStatus() removing shell interpretation via spawnSync, which is the primary objective of the PR.

✏️ 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.

Copy link

@coderabbitai coderabbitai bot left a comment

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 on lines +204 to +213
it("sandboxStatus does not pass sandbox name through bash -c", () => {
const src = fs.readFileSync(path.join(import.meta.dirname, "..", "bin", "nemoclaw.js"), "utf-8");
const fn = src.match(/function sandboxStatus\([\s\S]*?\n\}/);
expect(fn).toBeTruthy();
const body = fn[0];
// Must not use run() with template-literal sandbox name interpolation
expect(body).not.toMatch(/run\s*\(\s*`[^`]*\$\{/);
// Should use spawnSync with array args
expect(body).toMatch(/spawnSync\s*\(\s*"openshell"/);
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

These regression tests can pass without ever exercising sandboxStatus().

nemoclaw.js only routes status through sandboxStatus() after the sandbox lookup succeeds on Lines 480-499; otherwise it exits through the unknown-command path on Lines 501-515. This test never seeds or asserts that precondition, so test; touch ... status can pass without touching the new spawnSync("openshell", ...) call. The static check is also narrow enough that const cmd = ...; run(cmd) would slip through.

🔧 Tighten the static guard
-      // Must not use run() with template-literal sandbox name interpolation
-      expect(body).not.toMatch(/run\s*\(\s*`[^`]*\$\{/);
-      // Should use spawnSync with array args
-      expect(body).toMatch(/spawnSync\s*\(\s*"openshell"/);
+      expect(body).not.toMatch(/spawnSync\s*\(\s*"bash"\s*,\s*\[\s*"-c"/);
+      expect(body).not.toMatch(/shell\s*:\s*true/);
+      expect(body).not.toMatch(/\brun(?:Capture|Interactive)?\s*\(/);
+      expect(body).toMatch(/spawnSync\s*\(\s*"openshell"\s*,\s*\[\s*"sandbox"\s*,\s*"get"\s*,\s*sandboxName\s*\]/);

Please also back the e2e case with a registry fixture or a narrower seam so it provably hits the status action.

Also applies to: 215-233

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

In `@test/runner.test.js` around lines 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.

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.
Copy link

@coderabbitai coderabbitai bot left a comment

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant