fix(keychain): secure credentials fallback writes - #586
Conversation
Refuse POSIX symlink/non-regular credential paths and enforce mode 0600 before replacing persisted wallet passwords. Add regression coverage for existing permissions and symlink targets.\n\nFixes nansen-ai#585
pr-reviewer Summary for #d61493f✅ No issues found The code review completed successfully with no findings. Review effort: 3/5 (Moderate) SummaryThis PR hardens the POSIX fallback credential write path against two vulnerabilities: (1) permissions not being enforced when overwriting an existing Overall assessment: The implementation is correct. The fd lifecycle is clean — The regression tests are solid: they use a temporary Token usage: 21 input, 4,954 output, 339,491 cache read, 35,369 cache write | Usage Guide New pushes are reviewed automatically with a 10-minute cooldown between reviews. To request a review at any time, comment |
Set and assert the initial 0644 mode explicitly so the regression still exercises the vulnerable state under restrictive process umasks.
Summary
Fixes #585.
The wallet-password
.credentialsfallback previously usedfs.writeFileSync(path, ..., { mode: 0o600 }). On POSIX, the mode option applies only when creating a file, so an existing0644file remained broadly readable. The same path-based write also followed a final-component symlink and overwrote its target.Changes
O_NOFOLLOWto reject credential symlinksO_NONBLOCKand verify the opened descriptor is a regular file before modifying it0600before truncating/writing an existing credential file and again after writingO_NOFOLLOWsemantics are not portable0644files, symlink rejection, target preservation, and normal retrievalSecurity properties
The POSIX path validates the opened descriptor rather than doing a separate path check followed by a path-based write. This keeps the final-component symlink check and subsequent write attached to the same opened object.
The scope remains local: this prevents unsafe fallback-file state from preserving broad permissions or redirecting the credential write through a symlink.
Regression proof
With the new tests applied to the original implementation:
The failures showed:
420(0644) instead of384(0600){ stored: true, method: 'file' }instead of failingWith this fix:
Full suite:
Additional checks:
npm run lint— passedgit diff --check— passednpm pack --ignore-scripts --dry-run— passed; 85 package files, no test or audit artifacts includedAll security regressions used a temporary HOME, fake passwords, mocked OS-keychain failure, and no external network access.
Compatibility and risk
Checklist
npm test)src/schema.jsonupdated if new commands or flags were added — no command or flag changesREADME.mdupdated if new top-level commands or categories were added — no command changes.changeset/tighten-credentials-permissions.md)