Skip to content

Global Store mutation endpoint is unauthenticated (world-writable shared state, no CSRF) #33

Description

@LoboGuardian

Summary

The store mutation endpoint /api/_store/{api_id}/* accepts POST/PATCH/DELETE against any plain Store with no authorization. The store ID is embedded in every rendered page (SuperStore.inject_stores), so any visitor can read and overwrite shared server state — global Stores are effectively world-writable, and there is no CSRF token on the state-changing verbs.

Where

  • src/htealeaf/state/store.py:44 SuperStore.process — dispatches GET/POST/PATCH/DELETE with no auth check for plain Store.
  • src/htealeaf/state/store.py:22 inject_stores — emits new Store("<id>") into the page for every store, exposing the ID.

Notes / current mitigations

  • AuthStore is correctly partitioned per session via store.auth(session) — no IDOR there; this is specifically about plain Store.
  • SameSite=Lax on the session cookie blocks cross-site POST, so this is mostly a same-origin concern (any user can clobber another user's view of shared state). This may be intended for the "global shared reactive state" model.

Suggested direction

  • Document explicitly that a plain Store is global and unauthenticated shared state (not for per-user or trusted data).
  • Optionally add a CSRF token for state-changing verbs, and/or an opt-in authorization hook on Store for cases where global-but-guarded state is wanted.

Why it matters

Even if intended, the trust boundary should be explicit so users don't put sensitive or per-user data in a plain Store assuming it's protected. Found during a security review of develop.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions