-
Notifications
You must be signed in to change notification settings - Fork 60
[Med] fix(cli): make writeCredentials atomic and enforce 0600 on pre-existing files #712
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,9 +44,18 @@ export async function readCredentials(): Promise<Credentials | null> { | |
|
|
||
| export async function writeCredentials(creds: Credentials): Promise<void> { | ||
| const path = credentialsPath(); | ||
| await fs.mkdir(dirname(path), { recursive: true }); | ||
| // Mode 0600 — only the user can read. | ||
| await fs.writeFile(path, JSON.stringify(creds, null, 2), { encoding: 'utf8', mode: 0o600 }); | ||
| // Mode 0o700 on the directory — match local-vault.ts writeVault pattern. | ||
| await fs.mkdir(dirname(path), { recursive: true, mode: 0o700 }); | ||
| // Atomic write: write to a tmp file then rename, so a crash mid-write | ||
| // never leaves credentials.json truncated/corrupt. Mirrors writeVault() in | ||
| // local-vault.ts which holds equally sensitive data. | ||
| const tmp = `${path}.tmp`; | ||
| await fs.writeFile(tmp, JSON.stringify(creds, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 }); | ||
| await fs.rename(tmp, path); | ||
| // rename(2) preserves the source mode, but if the destination pre-existed | ||
| // at a looser mode (e.g. 0644 from an older sh1pt build), the resulting | ||
| // file keeps that loose mode. Explicitly tighten after rename. | ||
| await fs.chmod(path, 0o600).catch(() => {}); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| export async function clearCredentials(): Promise<void> { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment says "if the destination pre-existed at a looser mode, the resulting file keeps that loose mode" — this is incorrect.
rename(2)atomically replaces the destination with the source inode, so the result always gets the source (.tmp) file's permissions, not the destination's. The actual scenariochmodguards against is a stale.tmp: if a previous invocation crashed afterwriteFilebut beforerename, the.tmpfile persists, and becausefs.writeFile'smodeoption is only applied on file creation (not truncation of an existing file), a subsequent run'swriteFilecall won't tighten that file's mode — so after rename the destination inherits the stale mode. Thechmodcall itself is correct; only the explanation is wrong. The same inaccuracy was copied fromlocal-vault.ts, so both should be corrected together.