Skip to content

fix(security): download installers to file before execution#696

Merged
ericksoa merged 15 commits intomainfrom
fix/curl-bash-integrity
Mar 31, 2026
Merged

fix(security): download installers to file before execution#696
ericksoa merged 15 commits intomainfrom
fix/curl-bash-integrity

Conversation

@ericksoa
Copy link
Copy Markdown
Contributor

@ericksoa ericksoa commented Mar 23, 2026

Summary

Replace all curl | bash / curl | sudo bash patterns with download-to-tempfile-then-execute across the codebase.

Closes #574, #576, #577, #583.

File What changed
install.sh Ollama installer (2 locations)
scripts/install.sh NodeSource setup_22.x
scripts/brev-setup.sh NodeSource setup_22.x
bin/nemoclaw.js Remote uninstall fallback

Each location now uses mktemp -d, downloads the script to a file, executes from the file, and cleans up. SHA-256 pinning isn't practical for rolling-release upstream URLs, but download-then-execute prevents partial-download execution and allows inspection.

Test plan

  • 4 regression tests verify no curl | sh/bash/sudo patterns in any of the affected files
  • All existing tests pass
  • Manual: nemoclaw onboard with Ollama provider installs correctly
  • Manual: nemoclaw uninstall falls back to remote script correctly

Summary by CodeRabbit

  • Bug Fixes

    • Replaced unsafe piped curl|sh install/uninstall flows with a safer download‑then‑execute approach.
    • Added explicit download failure handling and guaranteed temporary‑file cleanup to reduce execution risk.
  • Tests

    • Added automated regression checks that scan install/uninstall scripts to detect and prevent reintroduction of curl‑to‑shell patterns.

Signed-off-by: Aaron Erickson aerickson@nvidia.com

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaced direct remote "curl | sh/bash" pipelines with download-to-temp-file-then-execute flows in multiple scripts, added explicit download error handling and guaranteed temp-dir cleanup, and added a regression test detecting curl ... | sh|bash pipeline occurrences.

Changes

Cohort / File(s) Summary
Uninstall script (Node)
bin/nemoclaw.js
Replaced streaming `curl ...
Ollama installer
install.sh
Replaced `curl -fsSL https://ollama.com/install.sh
NodeSource installer (brev-setup)
scripts/brev-setup.sh
Replaced piping NodeSource setup_22.x into sudo -E bash with: create temp dir, trap EXIT for cleanup, download setup script to local file, execute sudo -E bash <downloaded-script>; remaining package install steps unchanged.
NodeSource installer (scripts)
scripts/install.sh
Same change in install_node(): download setup_22.x to temp file then run sudo -E bash "$tmpdir/setup_node.sh" with cleanup on exit; apt-get install unchanged.
Regression test
test/runner.test.js
Added Vitest curl-pipe-to-shell guards that normalize files (strip comments, collapse continuations/pipelines), add shell/JS regexes to detect `curl ...

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐇
I dug a safe, snug temp den,
Fetched the script, then ran it when—
The bytes were local, checked, and done,
No blind pipe rushing on the run.
I burrow clean; the job is won. 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The PR addresses the core requirement from #574 by implementing download-then-execute for Ollama installer, but does not implement SHA-256 integrity verification as specified in the issue objectives. Consider adding SHA-256 pinning and verification for Ollama installer as outlined in issue #574, or document why it was not implemented due to upstream constraints.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main change: replacing curl-piped-to-shell patterns with a secure download-then-execute approach across multiple installer scripts.
Out of Scope Changes check ✅ Passed The PR extends the security fix beyond the originally linked issue (#574) to also fix similar patterns in NodeSource scripts (install.sh, brev-setup.sh) and bin/nemoclaw.js, which aligns with the stated objectives of closing issues #574, #576, #577, #583.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/curl-bash-integrity

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

Copy link
Copy Markdown
Contributor

@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: 4

🧹 Nitpick comments (1)
bin/nemoclaw.js (1)

243-248: Pin the fallback uninstall script to a versioned ref.

This path still executes REMOTE_UNINSTALL_URL, which points at refs/heads/main. Download-to-file avoids partial execution, but it also means an older client can end up running whatever uninstall script happens to be on main that day. Using the installed tag/commit or a release asset would make this fallback deterministic.

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

In `@bin/nemoclaw.js` around lines 243 - 248, The fallback currently downloads
REMOTE_UNINSTALL_URL (which points at refs/heads/main); change the download to a
deterministic, versioned URL by constructing or substituting a pinned ref (e.g.,
a RELEASE_TAG, package version, or the installed commit) before calling
execFileSync—update the variable used by execFileSync (REMOTE_UNINSTALL_URL) or
create a new pinnedUninstallUrl and use that when fetching into uninstallScript,
keeping the same execFileSync/ spawnSync flow with uninstallDir,
uninstallScript, and args so the fallback always retrieves a specific tag/commit
or release asset instead of main.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/nemoclaw.js`:
- Around line 246-255: The try/finally currently calls execFileSync and
exitWithSpawnResult inside the try so process.exit prevents the finally cleanup
and download errors leak stack traces and uninstallDir; refactor by isolating
the download: wrap the execFileSync( "curl", [...REMOTE_UNINSTALL_URL...,
uninstallScript]) call in its own try-catch that logs a clean user-friendly
error and exits (or throws) on failure, leave the spawnSync(...) invocation and
its result handling separate, move exitWithSpawnResult(result) to after the
try/finally that performs fs.rmSync(uninstallDir, { recursive: true, force: true
}) so cleanup always runs, and keep references to execFileSync, spawnSync,
exitWithSpawnResult, uninstallDir, REMOTE_UNINSTALL_URL, and uninstallScript to
locate and modify the code.

In `@scripts/brev-setup.sh`:
- Around line 41-44: The temporary directory created in tmpdir=$(mktemp -d) may
not be removed if curl or sudo -E bash "$tmpdir/setup_node.sh" fails; add a
cleanup trap that removes "$tmpdir" on exit/failure (e.g., trap 'rm -rf
"$tmpdir"' EXIT) immediately after creating tmpdir, and ensure tmpdir is
referenced in that trap so rm -rf "$tmpdir" always runs regardless of errors in
the subsequent curl or sudo -E bash commands; keep the existing rm -rf "$tmpdir"
for explicit cleanup after successful execution.

In `@scripts/install.sh`:
- Around line 235-238: The temporary directory created with tmpdir=$(mktemp -d)
can be leaked if curl or sudo -E bash fails; add a cleanup trap immediately
after creating tmpdir (e.g., trap 'rm -rf "$tmpdir"' EXIT) so rm -rf "$tmpdir"
runs on both success and failure, and ensure the trap is removed or left in
place as appropriate after cleanup; update the block containing tmpdir, mktemp
-d, curl -fsSL ... -o "$tmpdir"/setup_node.sh, and sudo -E bash
"$tmpdir"/setup_node.sh to rely on the trap for robust cleanup.

In `@test/runner.test.js`:
- Around line 213-245: The current per-line checks (isViolation, and the inline
filter in the bin/nemoclaw.js test) miss multiline curl-pipe-to-shell cases;
update the tests ("install.sh does not pipe curl to shell",
"scripts/install.sh...", "scripts/brev-setup.sh...", and "bin/nemoclaw.js does
not pipe curl to shell") to detect line-wrapped pipes by normalizing input first
(e.g., join logical shell continuations or remove trailing backslashes and join
adjacent lines, or collapse newline + indentation into a single space) before
applying the curl→shell regex, or alternatively run a multiline regex against
the whole file content to catch patterns like "curl ...
|\\s*\\n\\s*(sh|bash|sudo)"; apply this change where isViolation is defined and
where the inline filter is used so multiline piped shells are flagged.

---

Nitpick comments:
In `@bin/nemoclaw.js`:
- Around line 243-248: The fallback currently downloads REMOTE_UNINSTALL_URL
(which points at refs/heads/main); change the download to a deterministic,
versioned URL by constructing or substituting a pinned ref (e.g., a RELEASE_TAG,
package version, or the installed commit) before calling execFileSync—update the
variable used by execFileSync (REMOTE_UNINSTALL_URL) or create a new
pinnedUninstallUrl and use that when fetching into uninstallScript, keeping the
same execFileSync/ spawnSync flow with uninstallDir, uninstallScript, and args
so the fallback always retrieves a specific tag/commit or release asset instead
of main.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bc10a599-bd1b-4857-b393-5f3242266022

📥 Commits

Reviewing files that changed from the base of the PR and between c55a309 and a0af4d4.

📒 Files selected for processing (5)
  • bin/nemoclaw.js
  • install.sh
  • scripts/brev-setup.sh
  • scripts/install.sh
  • test/runner.test.js

@ericksoa ericksoa self-assigned this Mar 23, 2026
Copy link
Copy Markdown
Contributor

@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)
bin/nemoclaw.js (1)

247-253: ⚠️ Potential issue | 🟠 Major

Move failure exit out of the try block to guarantee temp-dir cleanup.

At Line 252, process.exit(1) is called inside the outer try; this can terminate before the finally at Line 259-260 runs, leaving uninstallDir behind on curl failures.

Suggested fix
   const uninstallScript = path.join(uninstallDir, "uninstall.sh");
   let result;
+  let downloadFailed = false;
   try {
     try {
       execFileSync("curl", ["-fsSL", REMOTE_UNINSTALL_URL, "-o", uninstallScript], { stdio: "inherit" });
     } catch {
       console.error(`  Failed to download uninstall script from ${REMOTE_UNINSTALL_URL}`);
-      process.exit(1);
+      downloadFailed = true;
     }
-    result = spawnSync("bash", [uninstallScript, ...args], {
-      stdio: "inherit",
-      cwd: ROOT,
-      env: process.env,
-    });
+    if (!downloadFailed) {
+      result = spawnSync("bash", [uninstallScript, ...args], {
+        stdio: "inherit",
+        cwd: ROOT,
+        env: process.env,
+      });
+    }
   } finally {
     fs.rmSync(uninstallDir, { recursive: true, force: true });
   }
+  if (downloadFailed) process.exit(1);
   exitWithSpawnResult(result);
#!/bin/bash
set -euo pipefail

# Inspect current control flow around the changed block.
cat -n bin/nemoclaw.js | sed -n '240,265p'

# Verify there is a process.exit call inside a try/finally structure.
ast-grep --lang javascript --pattern $'try { $$$ process.exit($_); $$$ } finally { $$$ }' bin/nemoclaw.js
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/nemoclaw.js` around lines 247 - 253, The current code calls
process.exit(1) inside the try block which can bypass the finally that cleans up
uninstallDir; change the flow so the try/finally always runs and exit happens
after finally: remove the process.exit(1) from inside the try, instead set a
local exitCode variable (e.g., let exitCode = 0) and in the inner catch handling
execFileSync/REMOTE_UNINSTALL_URL/uninstallScript set exitCode = 1 (and log the
error), then after the try/finally block check exitCode and call
process.exit(exitCode); this guarantees uninstallDir cleanup while preserving
the failure exit behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@bin/nemoclaw.js`:
- Around line 247-253: The current code calls process.exit(1) inside the try
block which can bypass the finally that cleans up uninstallDir; change the flow
so the try/finally always runs and exit happens after finally: remove the
process.exit(1) from inside the try, instead set a local exitCode variable
(e.g., let exitCode = 0) and in the inner catch handling
execFileSync/REMOTE_UNINSTALL_URL/uninstallScript set exitCode = 1 (and log the
error), then after the try/finally block check exitCode and call
process.exit(exitCode); this guarantees uninstallDir cleanup while preserving
the failure exit behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cb8d548b-de0d-4c8d-92fc-024c101462c9

📥 Commits

Reviewing files that changed from the base of the PR and between a0af4d4 and ce4044b.

📒 Files selected for processing (5)
  • bin/nemoclaw.js
  • install.sh
  • scripts/brev-setup.sh
  • scripts/install.sh
  • test/runner.test.js
✅ Files skipped from review due to trivial changes (2)
  • scripts/install.sh
  • test/runner.test.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • scripts/brev-setup.sh
  • install.sh

@brianwtaylor
Copy link
Copy Markdown
Contributor

brianwtaylor commented Mar 23, 2026

Very nice.

Curious about your thoughts on the NodeSource paths specifically: brev-setup.sh already does GPG-signed apt repo setup for the NVIDIA Container Toolkit (lines 61–65):

curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
  | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg

The same pattern works for NodeSource — add the GPG key + apt source directly instead of downloading and running their setup script:

curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
  | sudo gpg --dearmor -o /usr/share/keyrings/nodesource.gpg
echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" \
  | sudo tee /etc/apt/sources.list.d/nodesource.list > /dev/null
sudo apt-get update -qq && sudo apt-get install -y -qq nodejs

Replace all curl-pipe-to-shell patterns with download-to-tempfile:
- install.sh: Ollama installer (2 locations)
- scripts/install.sh: NodeSource setup
- scripts/brev-setup.sh: NodeSource setup
- bin/nemoclaw.js: remote uninstall fallback

Each uses mktemp -d, downloads to file, executes, cleans up.
Prevents partial-download execution and allows inspection.
SHA-256 pinning isn't practical for rolling-release upstream URLs.

4 regression tests verify no curl-pipe-to-shell in any of these files.

Closes #574, #576, #577, #583.
- Move exitWithSpawnResult outside try/finally in nemoclaw.js so
  process.exit does not bypass tmpdir cleanup
- Wrap download in inner try/catch for user-friendly error on failure
- Use subshell + trap EXIT for tmpdir cleanup in all shell scripts
  so set -euo pipefail exits don't leak temp directories
- Normalize test input (join continuations, collapse multiline pipes)
  to catch line-wrapped curl-pipe-to-shell regressions
- Tighten violation regex to match sh/bash/sudo bash but not sudo gpg
@cv cv force-pushed the fix/curl-bash-integrity branch from ce4044b to babfb2e Compare March 23, 2026 20:51
Copy link
Copy Markdown
Contributor

@cv cv left a comment

Choose a reason for hiding this comment

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

process.exit(1) on line 252 bypasses the finally cleanup. process.exit() in Node.js terminates immediately without executing finally blocks, so if the curl download fails, uninstallDir persists on disk. This is the scenario the second commit was meant to fix per its message ("Move exitWithSpawnResult outside try/finally so process.exit does not bypass tmpdir cleanup") — the fix was applied to exitWithSpawnResult but not to this process.exit(1). Replace with a throw or set a flag and exit after the finally.

Otherwise this looks ready to merge. The security scope is accurately described (partial-download prevention, not supply chain), cleanup is handled correctly in the shell scripts via subshell + trap, and the regression tests are well-constructed.

@wscurran wscurran added CI/CD Use this label to identify issues with NemoClaw CI/CD pipeline or GitHub Actions. enhancement New feature or request security Something isn't secure labels Mar 23, 2026
Main refactored scripts/install.sh into a thin wrapper delegating to
the root install.sh. The PR's NodeSource curl|bash fix for this file
is no longer needed since the logic was removed. Taking main's version.
@ericksoa
Copy link
Copy Markdown
Contributor Author

Reviewer guidance — specific risk areas to verify

  1. bin/nemoclaw.js uninstall fallback (line 508-525): The old code passed args through shellQuote() + string interpolation into bash -c. The new code passes args as an array to spawnSync. Verify no caller depends on shell expansion of uninstall args (unlikely, but worth confirming).

  2. install.sh Ollama subshells (lines 490, 504): Each uses trap 'rm -rf "$tmpdir"' EXIT inside a subshell (...) to avoid clobbering the parent script's traps. Verify the subshell scoping is sufficient — if a future edit removes the (...) parentheses, the parent's EXIT trap would be overwritten silently.

  3. install.sh Ollama stdin difference: The old curl | sh connected Ollama's installer stdin to the curl stream. The new version gives it an inherited terminal stdin. Ollama's installer does not read stdin, but this is a behavioral change worth noting.

  4. scripts/brev-setup.sh NodeSource (line 49): sudo -E bash "$tmpdir/setup_node.sh" replaces sudo -E bash -. The - (read stdin) is gone. Functionally equivalent since the script now comes from a file, but confirm NodeSource's setup script doesn't behave differently when invoked by path vs stdin.

  5. test/runner.test.js guard for scripts/install.sh (line 339): This test now passes trivially because scripts/install.sh is a 32-line wrapper with no curl. The guard still prevents reintroduction, but reviewers should know it's not actively testing a real fix in that file.

The sign-off was inside the Summary section where contributors
would delete it when writing their description. Move it to the
bottom after a separator with a CI-required comment so it
persists through edits.
@github-actions
Copy link
Copy Markdown

Brev E2E (full): FAILED on branch fix/curl-bash-integritySee logs

Instance e2e-pr-696 is still running. To SSH in:

brev refresh && ssh e2e-pr-696

When done, delete it: brev delete e2e-pr-696

@github-actions
Copy link
Copy Markdown

Brev E2E (full): FAILED on branch fix/curl-bash-integritySee logs

@ericksoa ericksoa added v0.0.1 and removed v0.0.1 labels Mar 31, 2026
@ericksoa ericksoa merged commit 3630013 into main Mar 31, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI/CD Use this label to identify issues with NemoClaw CI/CD pipeline or GitHub Actions. enhancement New feature or request security Something isn't secure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TP-SHELL-UNSAFE: Ollama Installer Downloaded Without Integrity Check

5 participants