Skip to content

fix(security): reject unsafe HOME fallback in credential storage#864

Open
fdzdev wants to merge 3 commits intoNVIDIA:mainfrom
fdzdev:fix/credentials-home-fallback
Open

fix(security): reject unsafe HOME fallback in credential storage#864
fdzdev wants to merge 3 commits intoNVIDIA:mainfrom
fdzdev:fix/credentials-home-fallback

Conversation

@fdzdev
Copy link

@fdzdev fdzdev commented Mar 25, 2026

Summary

  • When HOME is unset, credentials.js falls back to /tmp, making credentials.json world-readable (CWE-522, NVBUG 6002816)
  • Replaces eager module-level const CREDS_DIR = path.join(process.env.HOME || "/tmp", ...) with lazy getters that validate at access time
  • Rejects /tmp, /var/tmp, /dev/shm, and / as credential storage locations
  • Reads degrade gracefully (return empty {}); writes throw a clear error; module import never crashes

Test plan

  • NVIDIA_API_KEY in env + HOME unset → getCredential("NVIDIA_API_KEY") returns the env value (no file access)
  • HOME unset, no env var → getCredential() returns null (no crash)
  • HOME=/tmpsaveCredential() throws with actionable error message
  • HOME=/Users/x (normal) → credentials saved/loaded as before, mode 0600
  • External code importing CREDS_DIR / CREDS_FILE gets lazily resolved values via property getters

Summary by CodeRabbit

  • Bug Fixes
    • Improved credential storage security by validating and rejecting unsafe home-directory locations to avoid saving credentials to world-readable paths.
    • Enforced explicit filesystem permissions on credential directories and files (restricting access to the owner).
    • Made credential path resolution lazy and more robust, resolving and caching a validated home directory before producing credential paths.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Credential path resolution now validates and normalizes the user's home directory, rejects unsafe world-readable locations, and computes credential directory and file paths lazily. IO functions use the resolved paths; saving enforces directory (0700) and file (0600) permissions. Exports changed from constants to getter properties.

Changes

Cohort / File(s) Summary
Credentials Path & IO
bin/lib/credentials.js
Replaced static CREDS_DIR/CREDS_FILE with cached getter-backed getCredsDir()/getCredsFile(); added resolveHomeDir() (prefers process.env.HOME, falls back to os.homedir(), normalizes/realpaths and rejects unsafe locations /tmp, /var/tmp, /dev/shm, /); loadCredentials()/saveCredential() use computed paths; saveCredential() enforces fs.chmodSync on directory (0o700) and file (0o600); exports switched to getters.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I sniffed the home where secrets hide,
I mapped the path and then I pried,
I set the doors to lock up tight,
I kept the keys away from light,
A hopping guard through day and night.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main security fix: rejecting unsafe HOME fallback in credential storage, which directly matches the core change of adding validation to reject unsafe paths like /tmp.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/lib/credentials.js`:
- Around line 48-53: The write path in saveCredential (using getCredsDir,
getCredsFile, loadCredentials) relies on fs.writeFileSync's mode which only
applies on creation; ensure permissions are enforced on every save by calling
fs.chmodSync(file, 0o600) immediately after writing the JSON, so the credentials
file always ends up with strict 0600 permissions (keep the existing
fs.mkdirSync(dir, { mode: 0o700 }) behavior for the directory).
- Around line 10-22: Normalize and canonicalize the resolved HOME before
checking against UNSAFE_HOME_PATHS: inside resolveHomeDir(), take the computed
home (from process.env.HOME || os.homedir()), normalize it (e.g., remove
trailing slashes using path.resolve or path.normalize) and resolve symlinks to a
real path using fs.realpathSync (with a safe try/catch fallback to the
normalized path if realpath fails), then run the UNSAFE_HOME_PATHS.has() check
against that canonical path (and optional lowercase on case-insensitive
platforms) so values like "/tmp/" or symlinked aliases correctly match the
blocklist.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 274cf4d0-5e61-4e13-90b1-a42a6e656ab0

📥 Commits

Reviewing files that changed from the base of the PR and between da973d7 and fe252ca.

📒 Files selected for processing (1)
  • bin/lib/credentials.js

Facundo Fernandez added 2 commits March 25, 2026 18:13
When HOME is unset, credentials.js fell back to /tmp, making
credentials.json world-readable (CWE-522). Replace eager
module-level resolution with lazy getters that validate HOME
at access time and reject /tmp, /var/tmp, /dev/shm, and /.

Reads degrade gracefully (return empty); writes throw a clear
error. Module import never crashes.

NVBUG 6002816

Made-with: Cursor
- Canonicalize HOME via path.resolve + fs.realpathSync before
  blocklist check so /tmp/ (trailing slash) and symlinks are caught
- Add explicit fs.chmodSync after write to enforce 0o600/0o700
  permissions on existing files (writeFileSync mode only applies
  on creation)

Made-with: Cursor
@fdzdev fdzdev force-pushed the fix/credentials-home-fallback branch from b5bb2ca to 8844c96 Compare March 26, 2026 00:13
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 `@bin/lib/credentials.js`:
- Around line 279-282: saveCredential currently calls getCredsDir() and
getCredsFile() directly and can throw when resolveHomeDir() deems HOME unsafe;
wrap those calls in a try/catch inside saveCredential (catch the error, add
context like "saving credential failed: unsafe HOME" and rethrow or return a
rejected Promise consistently with saveCredential's API) and then update each
call site (onboard.js caller and the two callers in credentials.js) to handle
the thrown/rejected error (try/catch or .catch) and surface a user-friendly
message instead of assuming success.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 171eee63-2630-4d22-89e4-5538d01df797

📥 Commits

Reviewing files that changed from the base of the PR and between 8844c96 and 6ad5432.

📒 Files selected for processing (1)
  • bin/lib/credentials.js

Comment on lines +279 to +282
Object.defineProperty(exports_, "CREDS_DIR", { get: getCredsDir, enumerable: true });
Object.defineProperty(exports_, "CREDS_FILE", { get: getCredsFile, enumerable: true });

module.exports = exports_;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find all callers of saveCredential and check if they have error handling

# Find saveCredential calls and show surrounding context for error handling
rg -n -B3 -A3 'saveCredential\s*\(' --type=js

Repository: NVIDIA/NemoClaw

Length of output: 1536


🏁 Script executed:

# Check if CREDS_DIR or CREDS_FILE are destructured anywhere
rg -n "{\s*CREDS_DIR|{\s*CREDS_FILE|require.*CREDS" --type=js

Repository: NVIDIA/NemoClaw

Length of output: 177


🏁 Script executed:

# Find all getCredential calls and their context to check for error handling
rg -n -B2 -A2 "getCredential\s*\(" --type=js | head -80

Repository: NVIDIA/NemoClaw

Length of output: 4920


🏁 Script executed:

# Verify the actual implementation of saveCredential to confirm it can throw
cat -n bin/lib/credentials.js | sed -n '64,75p'

Repository: NVIDIA/NemoClaw

Length of output: 517


🏁 Script executed:

# Search for CREDS_DIR/CREDS_FILE destructuring with corrected regex
rg -n "CREDS_DIR|CREDS_FILE" --type=js | grep -E "require|import|const.*="

Repository: NVIDIA/NemoClaw

Length of output: 41


🏁 Script executed:

# Get the full implementation of getCredsDir, getCredsFile, and getCredential
cat -n bin/lib/credentials.js | sed -n '1,80p'

Repository: NVIDIA/NemoClaw

Length of output: 2891


🏁 Script executed:

# Check if resolveHomeDir can throw
cat -n bin/lib/credentials.js | sed -n '1,50p'

Repository: NVIDIA/NemoClaw

Length of output: 1958


🏁 Script executed:

# Get the exports section to verify what's exported
cat -n bin/lib/credentials.js | sed -n '260,282p'

Repository: NVIDIA/NemoClaw

Length of output: 760


🏁 Script executed:

# Check all places where credentials module is imported to see if anyone destructures CREDS_DIR/CREDS_FILE
rg -n "require.*credentials|from.*credentials" --type=js -A 2

Repository: NVIDIA/NemoClaw

Length of output: 4528


🏁 Script executed:

# Get full context of bin/nemoclaw.js import
cat -n bin/nemoclaw.js | sed -n '25,35p'

Repository: NVIDIA/NemoClaw

Length of output: 501


🏁 Script executed:

# Get full context of bin/lib/onboard.js import  
cat -n bin/lib/onboard.js | sed -n '30,40p'

Repository: NVIDIA/NemoClaw

Length of output: 660


saveCredential() can throw on unsafe HOME and lacks error handling at call sites.

The getter-based exports for CREDS_DIR and CREDS_FILE work correctly for lazy resolution. However, saveCredential() calls getCredsDir() and getCredsFile() directly (lines 65-66) without try/catch protection. These functions can throw from resolveHomeDir() if HOME is unsafe. The three call sites—onboard.js:1133, credentials.js:215, and credentials.js:262—have no error handling for this condition, though they do validate input before calling saveCredential().

Note: getCredential() is safe; it wraps getCredsFile() in a try/catch via loadCredentials(), so it will not throw even if HOME is unsafe.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/credentials.js` around lines 279 - 282, saveCredential currently
calls getCredsDir() and getCredsFile() directly and can throw when
resolveHomeDir() deems HOME unsafe; wrap those calls in a try/catch inside
saveCredential (catch the error, add context like "saving credential failed:
unsafe HOME" and rethrow or return a rejected Promise consistently with
saveCredential's API) and then update each call site (onboard.js caller and the
two callers in credentials.js) to handle the thrown/rejected error (try/catch or
.catch) and surface a user-friendly message instead of assuming success.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants