fix(security): stop leaking API keys in process args visible via ps aux#330
fix(security): stop leaking API keys in process args visible via ps aux#330
Conversation
📝 WalkthroughWalkthroughCredentials are moved out of command-line Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@test/credential-exposure.test.js`:
- Around line 56-65: The current credential detection uses a loose regex and a
prefix-based allowlist check which can miss unquoted KEY=VALUE args and allow
bypasses; update the regex used to produce credMatch so it reliably captures
unquoted and quoted KEY=VALUE forms (handle optional quotes/backticks and
optional surrounding braces) and normalize the captured value into fullValue
without dropping characters that could alter the token, then replace the
allowlist check (ALLOWED_CREDENTIAL_VALUES.some(...) with startsWith) with an
exact normalized comparison (normalize both allowed entries and fullValue and
use ===) so allowlisted items match only exactly and not by prefix; refer to the
variables/expressions credMatch, key, fullValue and ALLOWED_CREDENTIAL_VALUES
when making the changes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2c3ea473-9e61-465e-bad9-c40b62056432
📒 Files selected for processing (4)
bin/lib/onboard.jsnemoclaw-blueprint/orchestrator/runner.pynemoclaw/src/commands/onboard.tstest/credential-exposure.test.js
test/credential-exposure.test.js
Outdated
| const credMatch = line.match(/--credential[",\s]+["'`]?([A-Z_]+)=(.+?)["'`]/); | ||
| if (!credMatch) continue; | ||
|
|
||
| const key = credMatch[1]; | ||
| const fullValue = `${key}=${credMatch[2].replace(/["'`}\s]/g, "")}`; | ||
|
|
||
| // Check if this is in the allowlist | ||
| const isAllowed = ALLOWED_CREDENTIAL_VALUES.some((allowed) => | ||
| fullValue.startsWith(allowed.replace(/["']/g, "")) | ||
| ); |
There was a problem hiding this comment.
Harden credential literal detection to avoid false negatives.
The scanner can miss/bypass unsafe literals because allowlist matching is prefix-based and the regex can skip unquoted KEY=VALUE args. Tighten both checks so leaks can’t pass silently.
🔧 Proposed fix
- const credMatch = line.match(/--credential[",\s]+["'`]?([A-Z_]+)=(.+?)["'`]/);
+ const credMatch = line.match(/--credential[",\s]+["'`]?([A-Z_]+)=([^"'`\s,]+)/);
if (!credMatch) continue;
const key = credMatch[1];
- const fullValue = `${key}=${credMatch[2].replace(/["'`}\s]/g, "")}`;
+ const fullValue = `${key}=${credMatch[2].trim()}`;
// Check if this is in the allowlist
- const isAllowed = ALLOWED_CREDENTIAL_VALUES.some((allowed) =>
- fullValue.startsWith(allowed.replace(/["']/g, ""))
- );
+ const isAllowed = ALLOWED_CREDENTIAL_VALUES.includes(fullValue);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/credential-exposure.test.js` around lines 56 - 65, The current
credential detection uses a loose regex and a prefix-based allowlist check which
can miss unquoted KEY=VALUE args and allow bypasses; update the regex used to
produce credMatch so it reliably captures unquoted and quoted KEY=VALUE forms
(handle optional quotes/backticks and optional surrounding braces) and normalize
the captured value into fullValue without dropping characters that could alter
the token, then replace the allowlist check (ALLOWED_CREDENTIAL_VALUES.some(...)
with startsWith) with an exact normalized comparison (normalize both allowed
entries and fullValue and use ===) so allowlisted items match only exactly and
not by prefix; refer to the variables/expressions credMatch, key, fullValue and
ALLOWED_CREDENTIAL_VALUES when making the changes.
Closes shell-injection attack surface in the legacy CJS layer by replacing
all user-controlled run() / runCapture() shell strings with the new argv-safe
runArgv() / runCaptureArgv() helpers. assertSafeName() guards every
user-supplied sandbox/instance/preset name before it enters any command.
bin/lib/onboard.js -- all openshell/bash/brew calls -> runArgv;
file copies -> fs.cpSync/fs.rmSync (no cp shell)
bin/lib/nim.js -- docker pull/rm/run/stop/inspect -> runArgv/runCaptureArgv;
assertSafeName guard on sandboxName
bin/lib/policies.js -- openshell policy get/set -> runCaptureArgv/runArgv;
assertSafeName on sandboxName and presetName;
temp policy file written with mode 0o600
bin/nemoclaw.js -- setupSpark: remove inline NVIDIA_API_KEY=VALUE from
sudo argv (sudo -E already inherits env);
deploy: assertSafeName on instanceName;
sandbox connect/status/logs/destroy -> runArgv
Supersedes PRs: NVIDIA#148 (shell injection), part of NVIDIA#330 (credential leak).
… in argv Fixes: NVIDIA#325 (API key exposed in process list via ps aux) Supersedes: PRs NVIDIA#191, NVIDIA#330 The root cause: all three execution layers passed the actual credential VALUE as --credential KEY=VALUE, making it visible to any local user via `ps aux` or /proc/<pid>/cmdline. Safe pattern: set the secret in the child's inherited env, then pass only the env-var NAME to --credential (openshell env-lookup form). nemoclaw/src/commands/onboard.ts - process.env[credentialEnv] = apiKey before execOpenShell - --credential arg: credentialEnv (name only, not KEY=VALUE) - applies to both provider create and provider update paths nemoclaw-blueprint/orchestrator/runner.py - Rename credential_env -> target_cred_env with type-based fallback (nvidia -> NVIDIA_API_KEY, openai -> OPENAI_API_KEY) when not set in the blueprint profile. Supersedes PR NVIDIA#191's partial fix. - os.environ[target_cred_env] = credential before run_cmd - --credential arg: target_cred_env (name only) nemoclaw-blueprint/blueprint.yaml - Add credential_env: NVIDIA_API_KEY to the default profile. Without this field the type-based fallback would silently use OPENAI_API_KEY for the nvidia provider_type, causing auth failure. nemoclaw/src/onboard/config.ts - writeFileSync for config.json now passes mode: 0o600 so the file containing endpoint/model/credentialEnv metadata is not world-readable. test/credential-exposure.test.js (new file) - Static source scan: asserts no --credential KEY=VALUE pattern in any of the 3 execution layer files (allowlists dummy/ollama stubs) - Layer-specific structural checks (process.env set, os.environ set, blueprint default profile has credential_env) - Runtime injection PoC: proves old bash -c IS vulnerable; new runCaptureArgv IS NOT All 84 tests pass.
… in argv Fixes: NVIDIA#325 (API key exposed in process list via ps aux) Supersedes: PRs NVIDIA#191, NVIDIA#330 The root cause: all three execution layers passed the actual credential VALUE as --credential KEY=VALUE, making it visible to any local user via `ps aux` or /proc/<pid>/cmdline. Safe pattern: set the secret in the child's inherited env, then pass only the env-var NAME to --credential (openshell env-lookup form). nemoclaw/src/commands/onboard.ts - process.env[credentialEnv] = apiKey before execOpenShell - --credential arg: credentialEnv (name only, not KEY=VALUE) - applies to both provider create and provider update paths nemoclaw-blueprint/orchestrator/runner.py - Rename credential_env -> target_cred_env with type-based fallback (nvidia -> NVIDIA_API_KEY, openai -> OPENAI_API_KEY) when not set in the blueprint profile. Supersedes PR NVIDIA#191's partial fix. - os.environ[target_cred_env] = credential before run_cmd - --credential arg: target_cred_env (name only) nemoclaw-blueprint/blueprint.yaml - Add credential_env: NVIDIA_API_KEY to the default profile. Without this field the type-based fallback would silently use OPENAI_API_KEY for the nvidia provider_type, causing auth failure. nemoclaw/src/onboard/config.ts - writeFileSync for config.json now passes mode: 0o600 so the file containing endpoint/model/credentialEnv metadata is not world-readable. test/credential-exposure.test.js (new file) - Static source scan: asserts no --credential KEY=VALUE pattern in any of the 3 execution layer files (allowlists dummy/ollama stubs) - Layer-specific structural checks (process.env set, os.environ set, blueprint default profile has credential_env) - Runtime injection PoC: proves old bash -c IS vulnerable; new runCaptureArgv IS NOT All 84 tests pass.
Closes shell-injection attack surface in the legacy CJS layer by replacing
all user-controlled run() / runCapture() shell strings with the new argv-safe
runArgv() / runCaptureArgv() helpers. assertSafeName() guards every
user-supplied sandbox/instance/preset name before it enters any command.
bin/lib/onboard.js -- all openshell/bash/brew calls -> runArgv;
file copies -> fs.cpSync/fs.rmSync (no cp shell)
bin/lib/nim.js -- docker pull/rm/run/stop/inspect -> runArgv/runCaptureArgv;
assertSafeName guard on sandboxName
bin/lib/policies.js -- openshell policy get/set -> runCaptureArgv/runArgv;
assertSafeName on sandboxName and presetName;
temp policy file written with mode 0o600
bin/nemoclaw.js -- setupSpark: remove inline NVIDIA_API_KEY=VALUE from
sudo argv (sudo -E already inherits env);
deploy: assertSafeName on instanceName;
sandbox connect/status/logs/destroy -> runArgv
Supersedes PRs: NVIDIA#148 (shell injection), part of NVIDIA#330 (credential leak).
… in argv Fixes: NVIDIA#325 (API key exposed in process list via ps aux) Supersedes: PRs NVIDIA#191, NVIDIA#330 The root cause: all three execution layers passed the actual credential VALUE as --credential KEY=VALUE, making it visible to any local user via `ps aux` or /proc/<pid>/cmdline. Safe pattern: set the secret in the child's inherited env, then pass only the env-var NAME to --credential (openshell env-lookup form). nemoclaw/src/commands/onboard.ts - process.env[credentialEnv] = apiKey before execOpenShell - --credential arg: credentialEnv (name only, not KEY=VALUE) - applies to both provider create and provider update paths nemoclaw-blueprint/orchestrator/runner.py - Rename credential_env -> target_cred_env with type-based fallback (nvidia -> NVIDIA_API_KEY, openai -> OPENAI_API_KEY) when not set in the blueprint profile. Supersedes PR NVIDIA#191's partial fix. - os.environ[target_cred_env] = credential before run_cmd - --credential arg: target_cred_env (name only) nemoclaw-blueprint/blueprint.yaml - Add credential_env: NVIDIA_API_KEY to the default profile. Without this field the type-based fallback would silently use OPENAI_API_KEY for the nvidia provider_type, causing auth failure. nemoclaw/src/onboard/config.ts - writeFileSync for config.json now passes mode: 0o600 so the file containing endpoint/model/credentialEnv metadata is not world-readable. test/credential-exposure.test.js (new file) - Static source scan: asserts no --credential KEY=VALUE pattern in any of the 3 execution layer files (allowlists dummy/ollama stubs) - Layer-specific structural checks (process.env set, os.environ set, blueprint default profile has credential_env) - Runtime injection PoC: proves old bash -c IS vulnerable; new runCaptureArgv IS NOT All 84 tests pass.
Pass credential env var names to openshell instead of KEY=VALUE pairs. OpenShell reads the actual value from the environment, keeping it out of the process argument list. Closes #325
bf2529d to
9936c52
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (2)
test/credential-exposure.test.js (2)
29-31: Consider documenting regex coverage limitations.The current regexes are tailored to the patterns in use:
JS_EXPOSURE_REcatches quotedKEY=patternsJS_CREDENTIAL_CONCAT_REcatchesprocess.envinterpolationPY_EXPOSURE_REcatches f-string brace patternsThese work for the current code, but an unquoted pattern like
--credential KEY=VALUE(no quotes) would slip through. Since the tested files currently use quoted forms, this is acceptable—but a brief comment noting the assumed patterns would help future maintainers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/credential-exposure.test.js` around lines 29 - 31, Add a short comment above the regex definitions (JS_EXPOSURE_RE, JS_CREDENTIAL_CONCAT_RE, PY_EXPOSURE_RE) documenting their coverage and limitations: note they assume quoted JS credential patterns (KEY= inside quotes), process.env interpolation for concatenation, and Python f-string brace usage, and explicitly call out that unquoted forms like "--credential KEY=VALUE" will not be detected; this helps future maintainers understand the assumptions without changing the tests.
38-43: Unused variableiin filter callback.The line index parameter
iis declared but never used.🧹 Proposed fix
const violations = lines.filter( - (line, i) => + (line) => (JS_EXPOSURE_RE.test(line) || JS_CREDENTIAL_CONCAT_RE.test(line)) && // Allow comments that describe the old pattern !line.trimStart().startsWith("//"), );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/credential-exposure.test.js` around lines 38 - 43, The filter callback for computing violations declares an unused second parameter `i`; remove it (or replace it with `_`) from the parameter list of the arrow function used to build `violations` so the linter won't flag an unused variable—update the filter invocation that references JS_EXPOSURE_RE and JS_CREDENTIAL_CONCAT_RE accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@test/credential-exposure.test.js`:
- Around line 29-31: Add a short comment above the regex definitions
(JS_EXPOSURE_RE, JS_CREDENTIAL_CONCAT_RE, PY_EXPOSURE_RE) documenting their
coverage and limitations: note they assume quoted JS credential patterns (KEY=
inside quotes), process.env interpolation for concatenation, and Python f-string
brace usage, and explicitly call out that unquoted forms like "--credential
KEY=VALUE" will not be detected; this helps future maintainers understand the
assumptions without changing the tests.
- Around line 38-43: The filter callback for computing violations declares an
unused second parameter `i`; remove it (or replace it with `_`) from the
parameter list of the arrow function used to build `violations` so the linter
won't flag an unused variable—update the filter invocation that references
JS_EXPOSURE_RE and JS_CREDENTIAL_CONCAT_RE accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 53c58c50-e643-4f8c-a0f9-63dee202b491
📒 Files selected for processing (3)
bin/lib/onboard.jsnemoclaw-blueprint/orchestrator/runner.pytest/credential-exposure.test.js
…ux (NVIDIA#330) * fix(security): stop leaking API keys in process args visible via ps aux Pass credential env var names to openshell instead of KEY=VALUE pairs. OpenShell reads the actual value from the environment, keeping it out of the process argument list. Closes NVIDIA#325 * fix: address CodeRabbit nits in credential exposure test
…ux (NVIDIA#330) * fix(security): stop leaking API keys in process args visible via ps aux Pass credential env var names to openshell instead of KEY=VALUE pairs. OpenShell reads the actual value from the environment, keeping it out of the process argument list. Closes NVIDIA#325 * fix: address CodeRabbit nits in credential exposure test
Closes #325.
Summary
--credential NVIDIA_API_KEY=${value}to--credential NVIDIA_API_KEY(env-lookup form) so openshell reads the secret from the environment variable internally instead of receiving it as a CLI argument visible inps aux--credentialvaluesProblem
Any user on the system could run
ps aux | grep credentialand see the full NVIDIA API key in the process argument list:Fix
openshell's
--credentialflag supports two forms:--credential KEY=VALUE— literal value, visible inps aux--credential KEY— env-var lookup, openshell reads the value from the environmentThe fix sets the credential as an environment variable (not visible in
ps aux) and passes only the variable name to--credential.Non-secret dummy values (
OPENAI_API_KEY=dummy,OPENAI_API_KEY=ollama) are left inKEY=VALUEform since they are not real secrets.Files changed
bin/lib/onboard.jsNVIDIA_API_KEY=${...}→NVIDIA_API_KEYnemoclaw/src/commands/onboard.tsprocess.env[credentialEnv]before exec, pass var name onlynemoclaw-blueprint/orchestrator/runner.pyos.environ[target_cred_env]before run_cmd, pass var name onlytest/credential-exposure.test.jsTest plan
ps auxverified: 2,001 lines captured during onboard, zeronvapi-matchesOPENAI_API_KEY=dummy,OPENAI_API_KEY=ollama)Summary by CodeRabbit
Bug Fixes
Tests