-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Description
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:
path.join(ROOT, "uninstall.sh")— the project rootpath.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 -gwithout 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
mainbranch, or a network-level attack intercepting the HTTPS connection - Blast radius: Only affects users who run
nemoclaw uninstallwhen 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