Skip to content

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

Open
ericksoa wants to merge 2 commits intomainfrom
fix/curl-bash-integrity
Open

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

Conversation

@ericksoa
Copy link
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 shell installs/uninstalls with safer download-then-execute flows, adding error handling and cleanup to reduce execution risk.
  • Tests

    • Added automated checks that scan scripts for forbidden curl‑to‑shell patterns to prevent reintroduction of insecure installation flows.

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

coderabbitai bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

Replaced inline curl ... | sh|bash pipes with download-to-temp-file-then-execute flows in installers/uninstallers and added a test that scans for forbidden curl-to-shell piping to prevent regressions.

Changes

Cohort / File(s) Summary
Uninstall script (Node)
bin/nemoclaw.js
Replaced remote `curl
Ollama installer
install.sh
Replaced `curl ...
NodeSource installer (brev-setup)
scripts/brev-setup.sh
Replaced `curl ...
NodeSource installer (scripts)
scripts/install.sh
Same change as above for install_node() — download setup_22.x to temp file, execute with sudo -E bash "$tmpdir/setup_node.sh", and remove temp dir on exit.
Regression test
test/runner.test.js
Added a Vitest test that statically scans target files for forbidden `curl ...

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰
I dug a temp dir, soft and neat,
Saved the script before we meet.
No blind pipes rushing to run,
We fetch, then check, then safely done.
Hops of cleanup — tidy feat! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: replacing curl piping patterns with download-then-execute for security.
Linked Issues check ✅ Passed The PR addresses the core objective from #574 by replacing curl pipe patterns with download-then-execute across four files, though SHA-256 integrity checks were not implemented as noted in the PR.
Out of Scope Changes check ✅ Passed All changes are directly related to replacing curl piping patterns with secure download-then-execute sequences across installer scripts and test coverage.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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

@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
- 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
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)
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
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

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.

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

2 participants