Skip to content

fix(security): restrict keystore file permissions to 0o600 - #399

Open
yasinlex wants to merge 1 commit into
genlayerlabs:v0.40-devfrom
yasinlex:fix/keystore-file-permissions
Open

fix(security): restrict keystore file permissions to 0o600#399
yasinlex wants to merge 1 commit into
genlayerlabs:v0.40-devfrom
yasinlex:fix/keystore-file-permissions

Conversation

@yasinlex

@yasinlex yasinlex commented Jul 26, 2026

Copy link
Copy Markdown

Problem

Keystore files containing encrypted private keys are written via writeFileSync without specifying a file mode, leaving them with the default 0644 permissions — world-readable on POSIX systems. Any local user on the machine can read the encrypted keystore and attempt offline brute-force attacks on the password.

This affects three code paths:

  1. BaseAction.createKeypairByName() (line 222) — account creation
  2. ExportAccountAction.execute() (export.ts:55) — exporting an account to a keystore file
  3. ImportAccountAction.execute() (import.ts:72) — importing an account into a keystore file
// Before — world-readable (0644 on POSIX)
writeFileSync(keystorePath, encryptedJson);

Fix

Pass { mode: 0o600 } to writeFileSync and explicitly chmodSync to 0o600 afterwards:

// After — owner-only read/write (0600 on POSIX)
writeFileSync(keystorePath, encryptedJson, { mode: 0o600 });
try {
  chmodSync(keystorePath, 0o600);
} catch {
  // chmod can fail on Windows (no POSIX permissions)
}

The explicit chmodSync is necessary because writeFileSync does not change the mode of an already-existing file — so a file pre-created by an attacker with 0644 would keep world-read access even with the mode option. The chmod is wrapped in try/catch because it is a no-op on Windows (no POSIX permissions; Windows ACLs govern access there).

This is the same pattern the genswarms-telegram curl client already uses (File.chmod(config_path, 0o600)) for sensitive temp files.

Testing

  • TypeScript type check passes on all three modified files (pre-existing errors in unrelated stakingInfo.ts/StakingAction.ts are not affected)
  • No behavioral change to the happy path — only file permissions are restricted

Summary by CodeRabbit

  • Security Enhancements
    • Keystore files created, imported, or exported by the application now use restrictive file permissions to help prevent unauthorized access.
    • Permission enforcement is handled safely across platforms that do not support POSIX file permissions.

Keystore files containing encrypted private keys were written via
writeFileSync without specifying file mode, leaving them with the
default 0644 permissions — world-readable on POSIX systems. Any local
user could read the encrypted keystore and attempt offline brute-force
attacks on the password.

This affects three code paths:
- BaseAction.createKeypairByName() — account creation
- ExportAccountAction — exporting an account to a keystore file
- ImportAccountAction — importing an account into a keystore file

Fix: pass { mode: 0o600 } to writeFileSync and explicitly chmodSync to
0o600 afterwards. The chmod is necessary because writeFileSync does not
change the mode of an already-existing file, so a pre-created file with
0644 would keep world-read access. The chmod is wrapped in try/catch
because it is a no-op / can fail on Windows (no POSIX permissions).
@github-actions
github-actions Bot changed the base branch from main to v0.40-dev July 26, 2026 22:33
@github-actions

Copy link
Copy Markdown

This PR targeted main, which is only the default/static branch.

I retargeted it to v0.40-dev, the active development branch. Pushes to v0.40-dev automatically fast-forward main.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Keystore writes in account export, account import, and BaseAction now specify owner-only permissions (0o600) and attempt to enforce them with chmodSync, while ignoring platform-specific chmod failures.

Changes

Keystore Permission Enforcement

Layer / File(s) Summary
Restrictive keystore writes
src/commands/account/export.ts, src/commands/account/import.ts, src/lib/actions/BaseAction.ts
Keystore files are written with 0o600 permissions and passed through best-effort chmodSync enforcement, with failures caught for unsupported platforms.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: muncleuscles

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main security change: restricting keystore file permissions to 0o600.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/lib/actions/BaseAction.ts (1)

222-231: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Centralize the permissioned keystore write.

This write/chmod sequence is duplicated in src/commands/account/export.ts and src/commands/account/import.ts. A protected BaseAction helper would keep permission enforcement and failure handling consistent across all keystore paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/actions/BaseAction.ts` around lines 222 - 231, Centralize the
keystore write and permission enforcement in a protected helper on BaseAction,
moving the writeFileSync/chmodSync sequence and its existing chmod failure
handling there. Replace the duplicated sequences in the account export and
import flows with calls to this helper, preserving mode 0o600 and the current
Windows-compatible behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/actions/BaseAction.ts`:
- Around line 222-231: Centralize keystore writing and permission enforcement in
BaseAction, ensuring chmodSync failures propagate on non-Windows platforms
instead of being silently ignored; retain Windows-specific handling as
appropriate. Apply the shared fail-closed behavior at
src/lib/actions/BaseAction.ts lines 222-231 and 447-452, and update
src/commands/account/export.ts lines 63-68 and src/commands/account/import.ts
lines 65-70 to fail when permissions cannot be enforced rather than reporting
success.

---

Nitpick comments:
In `@src/lib/actions/BaseAction.ts`:
- Around line 222-231: Centralize the keystore write and permission enforcement
in a protected helper on BaseAction, moving the writeFileSync/chmodSync sequence
and its existing chmod failure handling there. Replace the duplicated sequences
in the account export and import flows with calls to this helper, preserving
mode 0o600 and the current Windows-compatible behavior.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 55ae6995-5e7a-45b9-a9ed-03650258589a

📥 Commits

Reviewing files that changed from the base of the PR and between 3396474 and 7daf5bd.

📒 Files selected for processing (3)
  • src/commands/account/export.ts
  • src/commands/account/import.ts
  • src/lib/actions/BaseAction.ts

Comment on lines +222 to +231
writeFileSync(keystorePath, encryptedJson, { mode: 0o600 });
// Enforce restrictive permissions even if the file already existed (writeFileSync
// does not change the mode of an existing file, so an attacker who pre-created it
// with 0644 would keep world-read access to the encrypted private key).
try {
chmodSync(keystorePath, 0o600);
} catch {
// chmod can fail on Windows (no POSIX permissions) — the encrypted keystore
// still protects the key, and Windows ACLs govern access there anyway.
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate files =="
git ls-files | rg '(^|/)(BaseAction|export|import)\.ts$|^src/(commands/account|lib/actions)/' || true

echo
echo "== Relevant snippets =="
for f in src/lib/actions/BaseAction.ts src/commands/account/export.ts src/commands/account/import.ts; do
  echo "--- $f $(wc -l < "$f") lines ---"
  sed -n '1,110p' "$f" | cat -n
  echo
done

echo
echo "== Search keystore permission/write patterns =="
rg -n "writeFileSync|chmodSync|chmod\\(|0o600|0600|encrypt|keystore" src -g '*.ts' || true

echo
echo "== Static verifier: writeFileSync+chmodSync failure handling =="
python3 - <<'PY'
from pathlib import Path
import re

files = [
    "src/lib/actions/BaseAction.ts",
    "src/commands/account/export.ts",
    "src/commands/account/import.ts",
]
for path in files:
    src = Path(path).read_text()
    write = list(re.finditer(r'writeFileSync\s*\(', src))
    chmod = list(re.finditer(r'chmodSync\s*\(', src))
    try_blocks = list(re.finditer(r'(?s)\btry\s*\{(?P<body>.*?)\}\s*\catch\s*\{', src))
    chmod_in_failing_try = []
    for m in try_blocks:
        if any(c.start() >= m.start() and c.start() <= m.end() for c in chmod):
            body = m.group('body')
            inside = bool(re.search(r'catch\s*\{.*?rethrow|\bthrow\s', body, re.S))
            chmod_in_failing_try.append((c.start(), c.start(), c.group().strip(), inside))
    print(f"\n{path}:")
    print(f"  writeFileSync count: {len(write)}")
    print(f"  chmodSync count: {len(chmod)}")
    for m in try_blocks:
        body = src[m.start():m.end()]
        if re.search(r'chmodSync\s*\(', body):
            has_rethrow = bool(re.search(r'catch\s*\{.*?rethrow|\bthrow\s', body, re.S))
            has_just_comment = bool(re.search(r'catch\s*\{[^}]*chmod can fail|chmod can fail[^}]*\}', body, re.S))
            print(f"  try/catch around chmod: has_throw_on_non_windows_reject={has_rethrow}, broad_catch_comment={has_just_comment}")
PY

Repository: genlayerlabs/genlayer-cli

Length of output: 34014


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== BaseAction tail around write/update and imports =="
sed -n '180,235p' src/lib/actions/BaseAction.ts | cat -n
sed -n '150,170p' src/lib/config/ConfigFileManager.ts | cat -n

echo
echo "== Import/export tail with static parser =="
python3 - <<'PY'
from pathlib import Path
import re

for path in ["src/lib/actions/BaseAction.ts", "src/commands/account/export.ts", "src/commands/account/import.ts"]:
    src = Path(path).read_text()
    matches = list(re.finditer(r'(?s)\btry\s*\{\s*chmodSync\s*\([^}]*\)\s*\}\s*\catch\s*\{', src))
    for m in matches:
        block = src[m.start():m.end()]
        try_rethrow = bool(re.search(r'\bthrow\s*;', block) or bool(re.search(r'\brethrow\b', block)))
        print(f"{path}: chmod try/catch: throw={try_rethrow}")
        print(block.replace("\n", " "))
    if not matches:
        print(f"{path}: no chmod try/catch matches")
PY

Repository: genlayerlabs/genlayer-cli

Length of output: 4928


Make keystore permission enforcement fail closed.

All three paths silently ignore chmodSync failures, so an existing keystore or exported file with broader permissions can keep the encrypted private key readable even though the command reports success.

  • src/lib/actions/BaseAction.ts#L447-L452: propagate non-Windows chmod failures and centralize the shared write logic.
  • src/commands/account/export.ts#L63-L68: fail closed when exported keystore permissions cannot be enforced.
  • src/commands/account/import.ts#L65-L70: fail closed when imported keystore permissions cannot be enforced.
📍 Affects 3 files
  • src/lib/actions/BaseAction.ts#L222-L231 (this comment)
  • src/commands/account/export.ts#L63-L68
  • src/commands/account/import.ts#L65-L70
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/actions/BaseAction.ts` around lines 222 - 231, Centralize keystore
writing and permission enforcement in BaseAction, ensuring chmodSync failures
propagate on non-Windows platforms instead of being silently ignored; retain
Windows-specific handling as appropriate. Apply the shared fail-closed behavior
at src/lib/actions/BaseAction.ts lines 222-231 and 447-452, and update
src/commands/account/export.ts lines 63-68 and src/commands/account/import.ts
lines 65-70 to fail when permissions cannot be enforced rather than reporting
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.

1 participant