-
Notifications
You must be signed in to change notification settings - Fork 2.1k
fix: validate openshell binary to prevent npm package shadowing #970
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,26 +4,52 @@ | |
| const { execSync } = require("child_process"); | ||
| const fs = require("fs"); | ||
|
|
||
| /** | ||
| * Verify that the binary at `binPath` is the OpenShell CLI and not another | ||
| * package that happens to share the same name (e.g. the npm `openshell` | ||
| * gateway package installed as a transitive dependency of `openclaw`). | ||
| * | ||
| * The OpenShell CLI prints a version string starting with "openshell " | ||
| * when invoked with `--version`. | ||
| */ | ||
| function isOpenshellCLI(binPath) { | ||
| try { | ||
| const out = execSync(`"${binPath}" --version`, { | ||
| encoding: "utf-8", | ||
| timeout: 5000, | ||
| stdio: ["ignore", "pipe", "ignore"], | ||
| }).trim(); | ||
| return /^openshell\s+\d+/.test(out); | ||
|
Comment on lines
+17
to
+22
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -n -C3 'execSync\(`"\$\{binPath\}" --version`' bin/lib/resolve-openshell.jsRepository: NVIDIA/NemoClaw Length of output: 268 🏁 Script executed: cat -n bin/lib/resolve-openshell.js | head -80Repository: NVIDIA/NemoClaw Length of output: 3189 Fix command injection vulnerability and add CLI validation to DI override path. Line 17 interpolates Suggested fixes-const { execSync } = require("child_process");
+const { execSync, execFileSync } = require("child_process");At line 17: - const out = execSync(`"${binPath}" --version`, {
+ const out = execFileSync(binPath, ["--version"], {
encoding: "utf-8",
timeout: 5000,
stdio: ["ignore", "pipe", "ignore"],
}).trim();At lines 54–56: } else if (opts.commandVResult && opts.commandVResult.startsWith("/")) {
- return opts.commandVResult;
+ if (checkCLI(opts.commandVResult)) return opts.commandVResult;
}🤖 Prompt for AI Agents |
||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Resolve the openshell binary path. | ||
| * | ||
| * Checks `command -v` first (must return an absolute path to prevent alias | ||
| * injection), then falls back to common installation directories. | ||
| * | ||
| * Every candidate is verified with `isOpenshellCLI()` to ensure the resolved | ||
| * binary is the real OpenShell CLI and not a same-named npm package. | ||
| * | ||
| * @param {object} [opts] DI overrides for testing | ||
| * @param {string|null} [opts.commandVResult] Mock result (undefined = run real command) | ||
| * @param {function} [opts.checkExecutable] (path) => boolean | ||
| * @param {function} [opts.checkCLI] (path) => boolean — override for `isOpenshellCLI` | ||
| * @param {string} [opts.home] HOME override | ||
| * @returns {string|null} Absolute path to openshell, or null if not found | ||
| */ | ||
| function resolveOpenshell(opts = {}) { | ||
| const home = opts.home ?? process.env.HOME; | ||
| const checkCLI = opts.checkCLI || isOpenshellCLI; | ||
|
|
||
| // Step 1: command -v | ||
| if (opts.commandVResult === undefined) { | ||
| try { | ||
| const found = execSync("command -v openshell", { encoding: "utf-8" }).trim(); | ||
| if (found.startsWith("/")) return found; | ||
| if (found.startsWith("/") && checkCLI(found)) return found; | ||
| } catch { /* ignored */ } | ||
| } else if (opts.commandVResult && opts.commandVResult.startsWith("/")) { | ||
| return opts.commandVResult; | ||
|
|
@@ -40,10 +66,10 @@ function resolveOpenshell(opts = {}) { | |
| "/usr/bin/openshell", | ||
| ]; | ||
| for (const p of candidates) { | ||
| if (checkExecutable(p)) return p; | ||
| if (checkExecutable(p) && checkCLI(p)) return p; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| module.exports = { resolveOpenshell }; | ||
| module.exports = { resolveOpenshell, isOpenshellCLI }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: NVIDIA/NemoClaw
Length of output: 169
🏁 Script executed:
Repository: NVIDIA/NemoClaw
Length of output: 6127
🏁 Script executed:
Repository: NVIDIA/NemoClaw
Length of output: 254
🏁 Script executed:
Repository: NVIDIA/NemoClaw
Length of output: 5528
Fallback install instructions are platform-incompatible and may fail due to incorrect asset naming and missing permission checks.
The hardcoded manual install commands (lines 1390–1392) assume Linux-musl and will fail on macOS where NemoClaw onboarding is supported. The asset name uses
$(uname -m)directly, butuname -mreturnsamd64orarm64on some systems, whereas OpenShell releases usex86_64andaarch64. Additionally, the fallback always attempts installation to/usr/local/binwithout checking write permissions; the installer script properly handles this by falling back to~/.local/binif needed.Align the fallback logic with
scripts/install-openshell.shby:uname -m→ release-compatible names)/usr/local/binwritability; fall back to~/.local/binif needed🤖 Prompt for AI Agents