fix(security): restrict keystore file permissions to 0o600 - #399
Conversation
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).
|
This PR targeted I retargeted it to |
📝 WalkthroughWalkthroughKeystore writes in account export, account import, and ChangesKeystore Permission Enforcement
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/lib/actions/BaseAction.ts (1)
222-231: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCentralize the permissioned keystore write.
This write/chmod sequence is duplicated in
src/commands/account/export.tsandsrc/commands/account/import.ts. A protectedBaseActionhelper 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
📒 Files selected for processing (3)
src/commands/account/export.tssrc/commands/account/import.tssrc/lib/actions/BaseAction.ts
| 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. | ||
| } |
There was a problem hiding this comment.
🔒 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}")
PYRepository: 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")
PYRepository: 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-L68src/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.
Problem
Keystore files containing encrypted private keys are written via
writeFileSyncwithout specifying a file mode, leaving them with the default0644permissions — 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:
BaseAction.createKeypairByName()(line 222) — account creationExportAccountAction.execute()(export.ts:55) — exporting an account to a keystore fileImportAccountAction.execute()(import.ts:72) — importing an account into a keystore fileFix
Pass
{ mode: 0o600 }towriteFileSyncand explicitlychmodSyncto0o600afterwards:The explicit
chmodSyncis necessary becausewriteFileSyncdoes not change the mode of an already-existing file — so a file pre-created by an attacker with0644would keep world-read access even with themodeoption. Thechmodis wrapped intry/catchbecause it is a no-op on Windows (no POSIX permissions; Windows ACLs govern access there).This is the same pattern the
genswarms-telegramcurl client already uses (File.chmod(config_path, 0o600)) for sensitive temp files.Testing
stakingInfo.ts/StakingAction.tsare not affected)Summary by CodeRabbit