Skip to content

HIGH: curl | bash Uninstall Fallback Without Integrity Check #577

@ReterAI

Description

@ReterAI

Description

HIGH: curl | bash Uninstall Fallback Without Integrity Check

Field Value
Severity High
CWE CWE-494 (Download of Code Without Integrity Check)
File bin/nemoclaw.js
Lines 203-226
Function uninstall(args)
Status Active

Description

When the local uninstall.sh script is not found on disk, the uninstall() function falls back to downloading and executing a remote script via curl | bash from a hardcoded GitHub URL. The downloaded script is executed without any integrity verification (no checksum, no signature).

This is a secondary curl|sh pattern — less severe than the NodeSource root execution (C-03) because it runs as the current user, not root.

Vulnerable Code

// bin/nemoclaw.js — line 29 (constant) + lines 203-226 (function)

const REMOTE_UNINSTALL_URL = "https://raw.githubusercontent.com/NVIDIA/NemoClaw/refs/heads/main/uninstall.sh";  // line 29

function uninstall(args) {  // line 203
  const localScript = resolveUninstallScript();
  if (localScript) {
    console.log(`  Running local uninstall script: ${localScript}`);
    const result = spawnSync("bash", [localScript, ...args], {
      stdio: "inherit",
      cwd: ROOT,
      env: process.env,
    });
    exitWithSpawnResult(result);
  }

  // ← Fallback: download and execute without verification
  console.log(`  Local uninstall script not found; falling back to ${REMOTE_UNINSTALL_URL}`);
  const forwardedArgs = args.map(shellQuote).join(" ");
  const command = forwardedArgs.length > 0
    ? `curl -fsSL ${shellQuote(REMOTE_UNINSTALL_URL)} | bash -s -- ${forwardedArgs}`
    : `curl -fsSL ${shellQuote(REMOTE_UNINSTALL_URL)} | bash`;
  const result = spawnSync("bash", ["-c", command], {
    stdio: "inherit",
    cwd: ROOT,
    env: process.env,
  });
  exitWithSpawnResult(result);
}

Positive: shellQuote() is used correctly

The URL and forwarded arguments are properly quoted via shellQuote() (line 31), preventing injection through the URL or args. The issue is purely about executing unverified remote code.

When the Fallback Triggers

The resolveUninstallScript() function (lines 35-48) checks two paths:

  1. path.join(ROOT, "uninstall.sh") — the project root
  2. path.join(__dirname, "..", "uninstall.sh") — relative to the bin directory

The fallback triggers when neither path exists, which can happen if:

  • NemoClaw was installed via npm install -g without the full repo
  • The user deleted the source directory but the CLI is still on PATH
  • A partial or corrupted installation

Recommended Fix

Option A: Download to temp file + verify hash

function uninstall(args) {
  const localScript = resolveUninstallScript();
  if (localScript) {
    // ... existing local path (unchanged)
  }

  // Download to temp file and verify
  const tmpFile = path.join(os.tmpdir(), `nemoclaw-uninstall-${Date.now()}.sh`);
  try {
    execSync(`curl -fsSL ${shellQuote(REMOTE_UNINSTALL_URL)} -o ${shellQuote(tmpFile)}`, {
      stdio: "pipe",
    });

    const hash = require("crypto")
      .createHash("sha256")
      .update(fs.readFileSync(tmpFile))
      .digest("hex");

    if (hash !== EXPECTED_UNINSTALL_HASH) {
      console.error(`  Integrity check failed for remote uninstall script`);
      console.error(`  Expected: ${EXPECTED_UNINSTALL_HASH}`);
      console.error(`  Actual:   ${hash}`);
      process.exit(1);
    }

    const result = spawnSync("bash", [tmpFile, ...args], {
      stdio: "inherit",
      cwd: ROOT,
      env: process.env,
    });
    exitWithSpawnResult(result);
  } finally {
    try { fs.unlinkSync(tmpFile); } catch {}
  }
}

Option B: Remove the fallback entirely

If the local script is missing, the installation is already broken. Printing an error with manual instructions may be safer than downloading from the network:

console.error("  Local uninstall script not found.");
console.error("  Re-clone the repo and run: bash uninstall.sh");
process.exit(1);

Risk Assessment

  • Impact: Arbitrary code execution with the current user's privileges
  • Exploitability: Requires compromise of the GitHub repository's main branch, or a network-level attack intercepting the HTTPS connection
  • Blast radius: Only affects users who run nemoclaw uninstall when the local script is missing — a narrow edge case
  • Mitigating factors: HTTPS protects the download. The URL points to a specific branch (refs/heads/main) in the official NVIDIA repository. shellQuote() prevents injection through the URL itself. The fallback is clearly logged to stdout before execution.

Reproduction Steps

Environment

Debug Output

-

Logs

Checklist

  • I confirmed this bug is reproducible
  • I searched existing issues and this is not a duplicate

Metadata

Metadata

Assignees

No one assigned

    Labels

    CI/CDUse this label to identify issues with NemoClaw CI/CD pipeline or GitHub Actions.bugSomething isn't workingpriority: highImportant issue that should be resolved in the next releasesecuritySomething isn't secure

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions