Repository navigation
Description
Finding
cold-store-reaper caches a live Google OAuth access token in state.json, while the README describes state.json as "(no secrets)" and the file is written without the restrictive permissions the package applies to real credentials. Anyone or anything that can read state.json (backups, sync tools, another process in a less-protected home) holds a working Drive bearer token for its lifetime (~1h).
Found by the pr-diff-review job reviewing PR #923 (cold-store-reaper v1), 2026-10-25; verified on current main (92d0c6ee).
Evidence
pkgs/cold-store-reaper/README.md:52:state.json # folder IDs + cached access token (no secrets)— the sentence contradicts itself: a cached access token is a credential.pkgs/cold-store-reaper/cold_store.py:332(inaccess_token()):state["access_token"] = token, persisted viasave_state()(lines 228-229) →_write_json()(line 207), a plainpath.write_text(...)with no mode restriction.- Contrast the package's own credential handling:
_write_private_json()writescredentials.json(client secret + refresh token) withos.open(..., 0o600)and an explicit chmod. The access token gets neither treatment. - The only hardening is
init()chmodding the home directory to0o700(line ~191 area) — that does not run on later invocations, and$COLD_STORE_HOMEmay point at a directory the tool did not create. - Note the v2 D1 code in the same file is careful to take
CLOUDFLARE_API_TOKEN"from the environment only; it is never stored" — the Drive access token deserves the same rule.
Why it matters
A short-lived token is still a live credential: with drive.file scope it can read/modify everything the cold-store app owns — which is the entire archive. The "(no secrets)" label invites copying state.json into places secrets must never go (a synced folder, a debug bundle, a repo).
Acceptance criteria
- The access token is no longer persisted in
state.json(keep it in memory only), ORstate.jsonis written with0o600permissions via the existing private-write helper and the README stops claiming "(no secrets)". - README's file-layout table accurately describes what
state.jsoncontains. - Existing tests still pass; a test or check confirms no token value lands in
state.jsonunder the chosen design (if in-memory-only).
— filed by the pr-diff-review job (PR #923); triage via the repo-health queue pilot.
Activity
dezren39 commentedon Oct 26, 2026
🤖 Agent note · filed
dezren39 commentedon Oct 26, 2026
Fixed in PR #988 (merged, merge commit
e9d2a6847bf674116ed59aa7bf0fe3b547aaef5c).access_token()now keeps the access token (and its expiry) in memory only — it is re-derived from the refresh token incredentials.jsonwhenever the in-memory copy is missing or about to expire. Nothing token-shaped is written tostate.json.save_state()writes via the existing_write_private_jsonhelper (0600) and strips any legacyaccess_tokenkey a pre-fix state file may still hold, so old tokens are purged on the next save;init()also createsstate.jsonprivately. The unused plain_write_jsonhelper was removed.state.jsonaccurately: mode 600, folder IDs + D1 pointers, no tokens (the "(no secrets)" wording is gone).TestStateHygiene): afteraccess_token()+ensure_folders()against the fake endpoints, the state.json bytes contain no token material and noaccess_tokenkey, and the file mode is 0600; a legacy state carrying a token is purged on save.