Skip to content

Harden CLI credential and login secret writes#537

Merged
riderx merged 2 commits intomainfrom
riderx/triage-attached-note
Mar 9, 2026
Merged

Harden CLI credential and login secret writes#537
riderx merged 2 commits intomainfrom
riderx/triage-attached-note

Conversation

@riderx
Copy link
Member

@riderx riderx commented Mar 8, 2026

This PR hardens Capgo CLI secret persistence against symlink-based overwrite attacks. It introduces shared safe file helpers for symbolic-link checks, atomic writes, and restricted permissions. Login now validates API keys before writing .capgo and persists keys only after successful verification. Credential storage now refuses symlink targets, writes atomically, and enforces 0700 directory plus 0600 file permissions for local credentials.

Summary by CodeRabbit

  • Bug Fixes
    • Strengthened credential and API key storage with more robust, atomic file operations
    • Enhanced protection against filesystem vulnerabilities in local configuration handling

@coderabbitai
Copy link

coderabbitai bot commented Mar 8, 2026

Warning

Rate limit exceeded

@riderx has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 23 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1e6321ca-07ac-4a70-957c-64d650ce93a4

📥 Commits

Reviewing files that changed from the base of the PR and between 9261003 and b280ed4.

📒 Files selected for processing (2)
  • src/login.ts
  • src/utils/safeWrites.ts
📝 Walkthrough

Walkthrough

Three files modified to enhance filesystem security. New module safeWrites.ts introduces atomic writes, symlink protection, and restricted permissions. Existing code in credentials.ts and login.ts refactored to use these safer utilities while maintaining API compatibility.

Changes

Cohort / File(s) Summary
Safe Filesystem Utilities
src/utils/safeWrites.ts
Introduces new module with secure filesystem operations: ensureNotSymlink for symlink detection, ensureSecureDirectory for safe directory creation, appendToSafeFile for appending content, writeFileAtomic for atomic writes via temp files with cleanup, and readSafeFile for safe file reading. All functions guard against symbolic links and enforce restricted default permissions (0o600).
Credentials Module
src/build/credentials.ts
Replaces direct filesystem calls with safe utilities: readFilereadSafeFile, mkdirensureSecureDirectory, writeFilewriteFileAtomic. Updates imports to include new safe-write functions.
Login Module
src/login.ts
Replaces synchronous file writes with atomic/safe async equivalents (writeFileAtomic, appendToSafeFile). Centralizes Supabase verification and login event emission before local/non-local branching logic, removing prior duplicate calls.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A hop through symlinks? No way, dear friend!
Atomic writes with temp files blend,
Safe permissions, 0o600 tight,
Clean up messes when things aren't right!
Security whiskers, twitching with care! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.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 pull request title 'Harden CLI credential and login secret writes' directly and accurately summarizes the main objective of the changeset, which is to enhance security of credential and login secret persistence through safer filesystem operations.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch riderx/triage-attached-note

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

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9261003aca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

src/login.ts Outdated
Comment on lines +47 to +53
await sendEvent(apikey, {
channel: 'user-login',
event: 'User CLI login',
icon: '✅',
user_id: userId,
notify: false,
}).catch()

Choose a reason for hiding this comment

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

P2 Badge Emit login telemetry only after login actually succeeds

loginInternal now calls sendEvent before validating --local repository constraints and before writing .capgo, so failed logins still produce a "User CLI login" event. For example, capgo login --local outside a git repo throws Not in a git repository but this path already reported a successful login, which can skew analytics and trigger downstream workflows on failed attempts.

Useful? React with 👍 / 👎.

Copy link

@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.

🧹 Nitpick comments (3)
src/utils/safeWrites.ts (3)

1-3: Minor: Fix import ordering per linter rules.

ESLint flags import ordering issues. The appendFile should come before writeFile, and node:crypto should come before node:path.

🧹 Proposed fix for import ordering
-import { chmod, lstat, mkdir, readFile, rename, rm, writeFile, appendFile } from 'node:fs/promises'
-import { dirname, join } from 'node:path'
-import { randomBytes } from 'node:crypto'
+import { appendFile, chmod, lstat, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
+import { randomBytes } from 'node:crypto'
+import { dirname, join } from 'node:path'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/safeWrites.ts` around lines 1 - 3, The import statements are out of
order per ESLint rules: place node:crypto imports before node:path and ensure
appendFile is imported before writeFile; update the import line to import
randomBytes from 'node:crypto' before importing dirname and join from
'node:path', and reorder the fs/promises named imports so appendFile appears
before writeFile (keeping other names—chmod, lstat, mkdir, readFile, rename,
rm—in their existing group). Locate the imports at the top of
src/utils/safeWrites.ts (references: appendFile, writeFile, randomBytes,
dirname, join) and reorder them accordingly to satisfy the linter.

37-50: Consider adding symlink check for temp file path.

The temp file path itself isn't checked for symlinks before writing. While the random 8-byte suffix makes collision extremely unlikely (~2^64 possibilities), for maximum defense-in-depth, consider guarding the temp path as well.

Additionally, dirname(filePath) could resolve through a symlink. If strict symlink avoidance is required for the entire path, consider validating the parent directory too.

🛡️ Optional: Add temp path symlink check
   const tempPath = join(dirname(filePath), `.capgo-tmp-${randomBytes(8).toString('hex')}`)
   try {
+    await ensureNotSymlink(tempPath)
     await writeFile(tempPath, content, { encoding: options.encoding ?? 'utf-8', mode })
     await rename(tempPath, filePath)
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/safeWrites.ts` around lines 37 - 50, The writeFileAtomic function
currently calls ensureNotSymlink(filePath) but doesn't validate the temp file or
parent dir; update writeFileAtomic to also validate the target directory and the
generated tempPath by calling ensureNotSymlink on dirname(filePath) (to avoid a
symlinked parent) and on tempPath before writeFile is invoked, so both the
parent directory and the temporary file path are checked for symlinks prior to
writing/renaming; keep the existing cleanup/rename logic and error handling
as-is.

10-21: Known limitation: TOCTOU race between symlink check and file operation.

There's a time-of-check to time-of-use (TOCTOU) window between ensureNotSymlink() and the subsequent file operation. An attacker could theoretically create a symlink after the check passes but before the write occurs.

For this CLI's threat model (local user credentials), the risk is low since:

  1. The attacker would need local access during the brief window
  2. writeFileAtomic uses a random temp filename, making the temp path unpredictable

Consider documenting this limitation. For stricter hardening, using O_NOFOLLOW flags via fs.open() would eliminate the race, but Node.js's high-level APIs don't expose this easily.

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

In `@src/utils/safeWrites.ts` around lines 10 - 21, Add a short comment above
ensureNotSymlink (and/or at top of src/utils/safeWrites.ts) documenting the
TOCTOU race: explain that a symlink could be created after the lstat check and
before a subsequent write, note why the risk is low for this CLI (local
attacker, random temp filenames used by writeFileAtomic), and mention that full
hardening would require using O_NOFOLLOW via fs.open or similar lower-level
APIs; optionally add a TODO linking to a ticket to implement O_NOFOLLOW-based
writes for stricter security.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/utils/safeWrites.ts`:
- Around line 1-3: The import statements are out of order per ESLint rules:
place node:crypto imports before node:path and ensure appendFile is imported
before writeFile; update the import line to import randomBytes from
'node:crypto' before importing dirname and join from 'node:path', and reorder
the fs/promises named imports so appendFile appears before writeFile (keeping
other names—chmod, lstat, mkdir, readFile, rename, rm—in their existing group).
Locate the imports at the top of src/utils/safeWrites.ts (references:
appendFile, writeFile, randomBytes, dirname, join) and reorder them accordingly
to satisfy the linter.
- Around line 37-50: The writeFileAtomic function currently calls
ensureNotSymlink(filePath) but doesn't validate the temp file or parent dir;
update writeFileAtomic to also validate the target directory and the generated
tempPath by calling ensureNotSymlink on dirname(filePath) (to avoid a symlinked
parent) and on tempPath before writeFile is invoked, so both the parent
directory and the temporary file path are checked for symlinks prior to
writing/renaming; keep the existing cleanup/rename logic and error handling
as-is.
- Around line 10-21: Add a short comment above ensureNotSymlink (and/or at top
of src/utils/safeWrites.ts) documenting the TOCTOU race: explain that a symlink
could be created after the lstat check and before a subsequent write, note why
the risk is low for this CLI (local attacker, random temp filenames used by
writeFileAtomic), and mention that full hardening would require using O_NOFOLLOW
via fs.open or similar lower-level APIs; optionally add a TODO linking to a
ticket to implement O_NOFOLLOW-based writes for stricter security.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a3ce58f3-132c-441f-8e6b-032681e92ead

📥 Commits

Reviewing files that changed from the base of the PR and between 9b3eb3a and 9261003.

📒 Files selected for processing (3)
  • src/build/credentials.ts
  • src/login.ts
  • src/utils/safeWrites.ts

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 8, 2026

@riderx riderx merged commit b8aa5cc into main Mar 9, 2026
19 checks passed
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.

1 participant