Write the desktop settings store owner-only - #1579
Open
GeiserX wants to merge 2 commits into
Open
Conversation
settings.json holds serverProfiles, which carries a remote server's bearer token or basic-auth password. conf defaults to 0o666, so the file was created 0644 -- readable by any other local account on Linux, where ~/.config is not reliably 0700. One option suffices: atomically chmods the temp inode when the requested mode differs from its default, and the rename carries it onto an existing loose file. Verified against the installed conf: no option -> 0644 fresh; configFileMode 0o600 -> 0600 fresh AND 0600 over an existing 0644 file. No automated test: this module and electron-store both import electron, which throws outside an Electron runtime, which is why the desktop package has no tests for these modules at all.
This was referenced Aug 13, 2026
Author
|
Context for this one: #1585 explains why this PR and the others in the series exist — they came out of a single pass over credential handling, asking for each credential where it ends up, how long it stays, and who can read it once it's there. This PR stands alone and doesn't depend on any of the others. |
…ore 0600 This PR shipped with no test and a paragraph explaining why testing it is impractical. That was true of the WIRING — settings.ts builds its store at module scope and pulls in electron, which cannot be imported outside an Electron runtime — but it was never true of the part carrying the risk. The fix rests on a claim about a transitive dependency: that `configFileMode` alone is enough, where the rest of this app uses a mode-plus-chmod pair, because `atomically` chmods the temp inode and the rename carries the tight mode onto an already-loose file. A claim about a dependency is worth checking rather than repeating, and it can be checked without importing settings.ts at all — by driving the real electron-store with the same options. Three tests: a fresh store is created 0600, an existing 0644 store is tightened on rewrite, and — the control — without the option the file really is world-readable. The control is what stops the first two passing for free on a machine with a restrictive umask. Mutation-checked: setting the option to 0o666 fails the first two and leaves the control green. What it deliberately does NOT bind is the wiring in settings.ts, and the file says so rather than leaving it to be discovered.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
settings.jsonholds a remote server's credential and was created world-readable.serverProfilescarries the bearer token or basic-auth password for any "Custom server" the user connects to.conf(under electron-store) defaults toconfigFileMode: 0o666, so with no explicit mode the file lands0644.One line:
configFileMode: 0o600on theStoreconstructor.Scope, stated honestly: this is primarily a Linux-desktop exposure. macOS is protected by
~/Librarybeing0700and Windows by ACLs on%APPDATA%. It is still worth fixing, because owner-only credential files are already this app's own standard —local-auth.ts(whichsettings.tsimports) writesauth.jsonat0o600plus achmod, and the sidecar manifest is chmodded the same way. This store was the outlier.Why one option and not the mode-plus-chmod pair used elsewhere
atomicallychmods the temp inode only when the requested mode differs from its own default, and the atomic rename then carries the tight mode onto an already-loose file. So setting the option is what turns that chmod on, and no follow-upchmodSyncis needed.I did not want to take that on trust, so I measured it against the installed
conf:0644configFileMode: 0o600, fresh file0600configFileMode: 0o600, file already06440600The third row is the one that matters for existing installs: a
settings.jsonwritten by an earlier build is tightened on the next write rather than staying loose forever.Tests
settings-permissions.test.ts— three of them, driving the realelectron-storewith the exactoptions
settings.tspasses:0600;0644store is tightened on rewrite — the half that would silently not happen if thesingle option did not trigger
atomically's chmod;would pass just as happily on a machine with a restrictive umask and prove nothing.
Mutation-checked: setting the option to
0o666fails the first two and leaves the control green.This pins the load-bearing part — the claim that one option is enough here, where the rest of the
app uses a mode-plus-chmod pair, because
atomicallychmods the temp inode and the rename carries thetight mode onto an already-loose file. A claim about a transitive dependency is worth checking rather
than repeating.
What it deliberately does not bind is the wiring in
settings.tsitself. That module builds itsstore at module scope and pulls in
electron, which cannot be imported outside an Electron runtime,which is why the desktop package has no
settings.test.ts(main/has tests forsupervised-connection,supervised-daemonandupdater-state, and nothing forsettings). Mockingelectron-storeaway would leave the test asserting the mock rather than the behaviour in question, sothe option is verified against the real dependency and the wiring is left uncovered on purpose.