Skip to content

Write the desktop settings store owner-only - #1579

Open
GeiserX wants to merge 2 commits into
UsefulSoftwareCo:mainfrom
GeiserX:fix/desktop-settings-permissions
Open

Write the desktop settings store owner-only#1579
GeiserX wants to merge 2 commits into
UsefulSoftwareCo:mainfrom
GeiserX:fix/desktop-settings-permissions

Conversation

@GeiserX

@GeiserX GeiserX commented Aug 13, 2026

Copy link
Copy Markdown

TL;DR

settings.json holds a remote server's credential and was created world-readable. serverProfiles carries the bearer token or basic-auth password for any "Custom server" the user connects to. conf (under electron-store) defaults to configFileMode: 0o666, so with no explicit mode the file lands 0644.

One line: configFileMode: 0o600 on the Store constructor.

Scope, stated honestly: this is primarily a Linux-desktop exposure. macOS is protected by ~/Library being 0700 and 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 (which settings.ts imports) writes auth.json at 0o600 plus a chmod, 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

atomically chmods 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-up chmodSync is needed.

I did not want to take that on trust, so I measured it against the installed conf:

case resulting mode
no option, fresh file 0644
configFileMode: 0o600, fresh file 0600
configFileMode: 0o600, file already 0644 0600

The third row is the one that matters for existing installs: a settings.json written 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 real electron-store with the exact
options settings.ts passes:

  • a fresh store is created 0600;
  • an existing 0644 store is tightened on rewrite — the half that would silently not happen if the
    single option did not trigger atomically's chmod;
  • the control: without the option, the file really is world-readable. Without that, the first two
    would pass just as happily on a machine with a restrictive umask and prove nothing.

Mutation-checked: setting the option to 0o666 fails 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 atomically chmods the temp inode and the rename carries the
tight 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.ts itself. That module builds its
store 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 for
supervised-connection, supervised-daemon and updater-state, and nothing for settings). Mocking
electron-store away would leave the test asserting the mock rather than the behaviour in question, so
the option is verified against the real dependency and the wiring is left uncovered on purpose.

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

GeiserX commented Aug 13, 2026

Copy link
Copy Markdown
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.
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