Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,16 +238,27 @@ function uninstall(args) {
exitWithSpawnResult(result);
}

// Download to file before execution — prevents partial-download execution.
// Upstream URL is a rolling release so SHA-256 pinning isn't practical.
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,
});
const uninstallDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-uninstall-"));
const uninstallScript = path.join(uninstallDir, "uninstall.sh");
let result;
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);
}
result = spawnSync("bash", [uninstallScript, ...args], {
stdio: "inherit",
cwd: ROOT,
env: process.env,
});
} finally {
fs.rmSync(uninstallDir, { recursive: true, force: true });
}
exitWithSpawnResult(result);
}

Expand Down
18 changes: 16 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -367,14 +367,28 @@ install_or_upgrade_ollama() {
info "Ollama v${current} meets minimum requirement (>= v${OLLAMA_MIN_VERSION})"
else
info "Ollama v${current:-unknown} is below v${OLLAMA_MIN_VERSION} — upgrading…"
curl -fsSL https://ollama.com/install.sh | sh
# Upstream URL is a rolling release so SHA-256 pinning isn't practical,
# but download-then-execute allows inspection and prevents partial-download execution.
(
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://ollama.com/install.sh -o "$tmpdir/install_ollama.sh"
sh "$tmpdir/install_ollama.sh"
)
info "Ollama upgraded to $(get_ollama_version)"
fi
else
# No ollama — only install if a GPU is present
if detect_gpu; then
info "GPU detected — installing Ollama…"
curl -fsSL https://ollama.com/install.sh | sh
# Upstream URL is a rolling release so SHA-256 pinning isn't practical,
# but download-then-execute allows inspection and prevents partial-download execution.
(
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://ollama.com/install.sh -o "$tmpdir/install_ollama.sh"
sh "$tmpdir/install_ollama.sh"
)
info "Ollama installed: v$(get_ollama_version)"
else
warn "No GPU detected — skipping Ollama installation."
Expand Down
9 changes: 8 additions & 1 deletion scripts/brev-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,14 @@ export DEBIAN_FRONTEND=noninteractive
# --- 0. Node.js (needed for services) ---
if ! command -v node >/dev/null 2>&1; then
info "Installing Node.js..."
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - >/dev/null 2>&1
# Upstream URL is a rolling release so SHA-256 pinning isn't practical,
# but download-then-execute allows inspection and prevents partial-download execution.
(
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://deb.nodesource.com/setup_22.x -o "$tmpdir/setup_node.sh"
sudo -E bash "$tmpdir/setup_node.sh" >/dev/null 2>&1
)
sudo apt-get install -y -qq nodejs >/dev/null 2>&1
info "Node.js $(node --version) installed"
else
Expand Down
9 changes: 8 additions & 1 deletion scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,14 @@ install_node() {
brew link --overwrite node@22 2>/dev/null || true
;;
nodesource)
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - >/dev/null 2>&1
# Upstream URL is a rolling release so SHA-256 pinning isn't practical,
# but download-then-execute allows inspection and prevents partial-download execution.
(
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://deb.nodesource.com/setup_22.x -o "$tmpdir/setup_node.sh"
sudo -E bash "$tmpdir/setup_node.sh" >/dev/null 2>&1
)
sudo apt-get install -y -qq nodejs >/dev/null 2>&1
;;
none)
Expand Down
57 changes: 57 additions & 0 deletions test/runner.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,4 +208,61 @@ describe("runner helpers", () => {
expect(!src.includes("execSync")).toBeTruthy();
});
});

describe("curl-pipe-to-shell guards (#574, #583)", () => {
// Strip comment lines, then join line continuations so multiline
// curl ... |\n bash patterns are caught by the single-line regex.
const stripComments = (src, commentPrefix) =>
src.split("\n").filter((l) => !l.trim().startsWith(commentPrefix)).join("\n");

const joinContinuations = (src) =>
src.replace(/\\\n\s*/g, " ");

const collapseMultilinePipes = (src) =>
src.replace(/\|\s*\n\s*/g, "| ");

const normalize = (src, commentPrefix) =>
collapseMultilinePipes(joinContinuations(stripComments(src, commentPrefix)));

const shellViolationRe = /curl\s[^|]*\|\s*(sh|bash|sudo\s+(-\S+\s+)*(sh|bash))\b/;
const jsViolationRe = /curl.*\|\s*(sh|bash|sudo\s+(-\S+\s+)*(sh|bash))\b/;

const findShellViolations = (src) => {
const normalized = normalize(src, "#");
return normalized.split("\n").filter((line) => {
const t = line.trim();
if (t.startsWith("printf") || t.startsWith("echo")) return false;
return shellViolationRe.test(t);
});
};

const findJsViolations = (src) => {
const normalized = normalize(src, "//");
return normalized.split("\n").filter((line) => {
const t = line.trim();
if (t.startsWith("*")) return false;
return jsViolationRe.test(t);
});
};

it("install.sh does not pipe curl to shell", () => {
const src = fs.readFileSync(path.join(import.meta.dirname, "..", "install.sh"), "utf-8");
expect(findShellViolations(src)).toEqual([]);
});

it("scripts/install.sh does not pipe curl to shell", () => {
const src = fs.readFileSync(path.join(import.meta.dirname, "..", "scripts", "install.sh"), "utf-8");
expect(findShellViolations(src)).toEqual([]);
});

it("scripts/brev-setup.sh does not pipe curl to shell", () => {
const src = fs.readFileSync(path.join(import.meta.dirname, "..", "scripts", "brev-setup.sh"), "utf-8");
expect(findShellViolations(src)).toEqual([]);
});

it("bin/nemoclaw.js does not pipe curl to shell", () => {
const src = fs.readFileSync(path.join(import.meta.dirname, "..", "bin", "nemoclaw.js"), "utf-8");
expect(findJsViolations(src)).toEqual([]);
});
});
});