Skip to content

cold-store-reaper: live Google access token is cached in state.json, which the README calls '(no secrets)' and which is written without 0600 permissions #940

Closed

Description

@dezren39

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 (in access_token()): state["access_token"] = token, persisted via save_state() (lines 228-229) → _write_json() (line 207), a plain path.write_text(...) with no mode restriction.
  • Contrast the package's own credential handling: _write_private_json() writes credentials.json (client secret + refresh token) with os.open(..., 0o600) and an explicit chmod. The access token gets neither treatment.
  • The only hardening is init() chmodding the home directory to 0o700 (line ~191 area) — that does not run on later invocations, and $COLD_STORE_HOME may 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), OR state.json is written with 0o600 permissions via the existing private-write helper and the README stops claiming "(no secrets)".
  • README's file-layout table accurately describes what state.json contains.
  • Existing tests still pass; a test or check confirms no token value lands in state.json under the chosen design (if in-memory-only).

— filed by the pr-diff-review job (PR #923); triage via the repo-health queue pilot.

Activity

  1. added
    kind/securitySecurity issue or security hardening
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/lowNice to have; do when convenient
    queue/queuedHas a queued task in the Supabase agent queue
    app/cold-store-reaperPackage/app: cold-store-reaper (pkgs/ or apps/ in this repo)
    on Oct 26, 2026
  2. dezren39 commented on Oct 26, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: pr-diff-review job
    • Session / ID: pr-diff-review (cron job_id pr-diff-review) run 2026-10-25 20:37 CDT
    • Found via: filed this run
    • Task: Triage the cold-store access token cached in state.json (PR cold-store-reaper v1: Google Drive cold-store CLI (pack / resumable upload+verify / range fetch) #923)
    • Expectation: effort/s — triage verdict on the finding against current main, then a fix or a documented acceptance
    • Links: source PR evidence is in the issue body; a repo-task/v1 triage packet for this issue is enqueued in repo-health-code-pilot
  3. dezren39 commented on Oct 26, 2026

    @dezren39
    MemberAuthor

    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 in credentials.json whenever the in-memory copy is missing or about to expire. Nothing token-shaped is written to state.json.
    • save_state() writes via the existing _write_private_json helper (0600) and strips any legacy access_token key a pre-fix state file may still hold, so old tokens are purged on the next save; init() also creates state.json privately. The unused plain _write_json helper was removed.
    • README file layout now describes state.json accurately: mode 600, folder IDs + D1 pointers, no tokens (the "(no secrets)" wording is gone).
    • Tests (TestStateHygiene): after access_token() + ensure_folders() against the fake endpoints, the state.json bytes contain no token material and no access_token key, and the file mode is 0600; a legacy state carrying a token is purged on save.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/cold-store-reaperPackage/app: cold-store-reaper (pkgs/ or apps/ in this repo)kind/securitySecurity issue or security hardeningpriority/lowNice to have; do when convenientqueue/queuedHas a queued task in the Supabase agent queuesource/pr-diff-reviewFiled by the pr-diff-review jobstatus/needs-triageNot yet triaged; needs a first looktask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions