mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-08-07 11:55:27 +00:00
Follow-up hardening beyond the explicit review findings: - Propagate session revocation across uvicorn workers: token validation now syncs issuance AND revocation from sessions.json (mtime-gated), _save_sessions merges on-disk state under an inter-process flock so concurrent workers can't lose each other's sessions, and revocation tombstones prevent a just-revoked token from being re-merged. - Restrict sessions.json and auth.json to 0600 (bearer tokens and password hashes; same policy as data/app.db, #4420), applied atomically at write time and retroactively at load. - Password-login session cookie: SECURE_COOKIES=false can no longer downgrade the cookie when the request arrived over HTTPS (spoofable X-Forwarded-Proto still requires TRUST_PROXY_HEADERS opt-in). - Document why OIDC state tokens are deliberately not single-use and which mechanisms bound the replay window. - Warn once per process (not twice per login) when OIDC_ALLOW_INSECURE_COOKIES is enabled; pass the variable through the Compose files so the documented dev override actually reaches containers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GRiLb12nnLnBnYsg14oSWd
57 lines
2.1 KiB
Python
57 lines
2.1 KiB
Python
"""Atomic JSON file writes.
|
|
|
|
Use this everywhere a JSON config file is persisted. A plain `open("w") +
|
|
json.dump` truncates the file on first write and only fills it with new
|
|
content afterwards — a kill -9 / power loss / OOM in between produces a
|
|
truncated or empty file. For password DBs (`auth.json`) and live state
|
|
(`sessions.json`, `settings.json`, `integrations.json`, `cookbook_state.json`),
|
|
that's a data-loss event.
|
|
|
|
`atomic_write_json` writes to a sibling tmp file, fsyncs, then `os.replace`s
|
|
into place. On POSIX `os.replace` is atomic on the same filesystem.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from typing import Any, Optional
|
|
|
|
|
|
def atomic_write_json(
|
|
path: str, data: Any, *, indent: Optional[int] = None, mode: Optional[int] = None
|
|
) -> None:
|
|
"""Atomically persist `data` as JSON at `path`.
|
|
|
|
The temp file uses the live PID as a suffix so two processes saving the
|
|
same file (e.g. unit tests) don't collide on the rename target.
|
|
|
|
When *mode* is given (e.g. ``0o600`` for files holding secrets), the
|
|
temp file is chmod'ed before the rename so the restricted permissions
|
|
are in place atomically with the content — there is no window where
|
|
the target exists with default-umask permissions.
|
|
"""
|
|
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
|
tmp = f"{path}.tmp.{os.getpid()}"
|
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
if mode is not None:
|
|
try:
|
|
os.fchmod(f.fileno(), mode)
|
|
except AttributeError: # Windows has no fchmod
|
|
os.chmod(tmp, mode)
|
|
json.dump(data, f, indent=indent)
|
|
f.flush()
|
|
os.fsync(f.fileno())
|
|
os.replace(tmp, path)
|
|
|
|
|
|
def atomic_write_text(path: str, text: str) -> None:
|
|
if not isinstance(text, str):
|
|
raise TypeError("atomic_write_text expects a string")
|
|
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
|
tmp = f"{path}.tmp.{os.getpid()}"
|
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
f.write(text)
|
|
f.flush()
|
|
os.fsync(f.fileno())
|
|
os.replace(tmp, path)
|