What happened?
SecretStore._write() and write_private_text() in coworker/secrets.py both write
the secrets/.tmp file to disk before calling _restrict_to_user() to chmod it to
0600:
https://github.com/andrewyng/openworker/blob/main/coworker/secrets.py#L99-L102
https://github.com/andrewyng/openworker/blob/main/coworker/secrets.py#L190-L194
tmp = self.path.with_name(self.path.name + ".tmp")
tmp.write_text(json.dumps(store, indent=2), encoding="utf-8") # default umask perms
_restrict_to_user(tmp, is_dir=False) # chmod 0600 after
os.replace(tmp, self.path)
Path.write_text() creates the file using the process umask (commonly 0644 on
Linux/macOS), so for the moment between the write and the chmod, the plaintext
secrets/tokens file is world- or group-readable on disk. On a shared/multi-user
machine, another local user (or a background process) could read connector/model
credentials during that window.
Separately, in _write(), _restrict_to_user(tmp, is_dir=False) isn't wrapped in
try/except OSError the way the parent-directory chmod above it is. If it raises,
the .tmp file — containing full plaintext secrets — is left on disk at default
permissions indefinitely, since os.replace() never runs to move/overwrite it.
The module docstring states intent clearly ("v1 is a 0600 JSON file"), so this
looks like an ordering oversight rather than a design choice.
Suggested fix
Create the tmp file already restricted instead of restricting after the fact, e.g.:
fd = os.open(str(tmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(json.dumps(store, indent=2))
This removes the window entirely on POSIX (no separate chmod is needed), and the
Windows ACL step can stay as-is afterward for defense in depth.
Environment
- macOS / Linux (any POSIX system with default umask 022 or looser)
- Affects
coworker/secrets.py on current main
What happened?
SecretStore._write()andwrite_private_text()incoworker/secrets.pyboth writethe secrets/
.tmpfile to disk before calling_restrict_to_user()to chmod it to0600:
https://github.com/andrewyng/openworker/blob/main/coworker/secrets.py#L99-L102
https://github.com/andrewyng/openworker/blob/main/coworker/secrets.py#L190-L194
Path.write_text()creates the file using the process umask (commonly0644onLinux/macOS), so for the moment between the write and the chmod, the plaintext
secrets/tokens file is world- or group-readable on disk. On a shared/multi-user
machine, another local user (or a background process) could read connector/model
credentials during that window.
Separately, in
_write(),_restrict_to_user(tmp, is_dir=False)isn't wrapped intry/except OSErrorthe way the parent-directory chmod above it is. If it raises,the
.tmpfile — containing full plaintext secrets — is left on disk at defaultpermissions indefinitely, since
os.replace()never runs to move/overwrite it.The module docstring states intent clearly ("v1 is a
0600JSON file"), so thislooks like an ordering oversight rather than a design choice.
Suggested fix
Create the tmp file already restricted instead of restricting after the fact, e.g.:
This removes the window entirely on POSIX (no separate chmod is needed), and the
Windows ACL step can stay as-is afterward for defense in depth.
Environment
coworker/secrets.pyon currentmain