Skip to content

Create the state directory private - #9

Merged
evanpurkhiser merged 2 commits into
evanpurkhiser:mainfrom
joaodrp:fix/private-state-and-credentials
Sep 2, 2026
Merged

evanpurkhiser merged 2 commits into
evanpurkhiser:mainfrom
joaodrp:fix/private-state-and-credentials

Conversation

@joaodrp

@joaodrp joaodrp commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

On a stock install the append log lands world-readable:

755  ~/.local/state/things3
600  ~/.local/state/things3/auth.json
755  ~/.local/state/things3/append-log
644  ~/.local/state/things3/append-log/things.log

things.log holds every task title and note in plaintext, so any other local
user can read it. auth.json was the only thing already protected.

create_private_dir narrows the directory to 0700 when creating it, and
tightens an existing one on upgrade, which covers the log, both caches and the
staging files at once.

Separately, write_auth wrote the password at the process umask and narrowed
the mode only after the rename, leaving it briefly world-readable. The staging
path is also a fixed .tmp name, so anything able to write the directory can
pre-plant a symlink and have the password written through it. It now creates the
staging file with create_new at 0600 and lets the rename carry the mode over.

Two tests cover the directory; a third plants a symlink at the staging path and
asserts the target is untouched. That one fails on the current implementation.

`things.log` is opened without an explicit mode and the state directory is
created with `create_dir_all`, so on a default umask they land at 0644 in a
0755 directory. That log holds every task title and note in plaintext,
readable by any other local user; `auth.json` was the only thing already
protected.

`create_private_dir` narrows the directory it creates to 0700, tightening an
existing one on upgrade too. The auth path applies it to the state directory
itself; the sync path applies it to `append-log`, which leaves the state
directory a plain intermediate when no `set-auth` has run. Its contents are
0600/0700 either way.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a53ceb6124

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/auth.rs
perms.set_mode(0o600);
fs::set_permissions(&path, perms)?;
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve owner permissions despite a restrictive umask

When the process umask masks owner bits (for example, umask 0777), OpenOptionsExt::mode(0o600) creates the file with those bits removed because the requested mode is still filtered through the umask. The previous post-rename set_permissions(0o600) restored an exact readable mode, whereas this version can leave auth.json at 000, causing subsequent load_auth calls to fail. Set the staging file's permissions to 0600 after opening but before writing the password so there is no exposure window.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in b4558bf: the mode is restated between open and write. Verified the mode test passes under umask 0777, which previously produced 000.

`write_auth` wrote the payload at the process umask and narrowed the mode
only after the rename, leaving the plaintext password world-readable in
between. The staging path is also a fixed `.tmp` name, so anything able to
write the directory can pre-plant a symlink and have the password written
through it.

Create the staging file with `create_new` at `0600` instead, then restate the
mode before writing: `open` filters the requested mode through the umask, so
a restrictive one would otherwise leave the file unreadable to its owner. The
rename carries the mode onto the final file.

Adds a regression test that plants a symlink at the staging path and
asserts the target is untouched. It fails on the previous implementation.
@joaodrp
joaodrp force-pushed the fix/private-state-and-credentials branch from a53ceb6 to b4558bf Compare September 2, 2026 20:05
@evanpurkhiser
evanpurkhiser merged commit 4cec09a into evanpurkhiser:main Sep 2, 2026
11 checks passed
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.

2 participants