Skip to content

oauth::verify_state("", "") returns true — CSRF-state footgun, not currently reachable #13

Closed

Description

@jjbaumert

Summary: crates/auth/src/oauth.rs::verify_state returns true when both received and expected are the empty string. A caller that maps "no stored state" to an empty string on both sides (instead of rejecting absent state before comparing) would pass CSRF-state verification incorrectly. I verified this is not currently reachable in production: the live OAuth callback route (crates/api/src/routes/auth.rs::handle_callback / callback_provider) never calls verify_state at all — it uses a Redis-backed GETDEL (take_oauth_flow) that atomically reads and erases the pending-flow record, and explicitly rejects with ApiError::BadRequest via .ok_or_else(...) when nothing is found, before any string comparison would happen. verify_state itself appears to be unused outside its own test module today.

Severity: S4 — latent/internal footgun. Not exploitable via any current production code path; the risk is a future caller (a refactor of the callback route, or a new integration reusing this helper) reintroducing the pattern this function is meant to guard against. Already pinned by a warning-commented test (verify_state_edge_cases, crates/auth/src/oauth.rs:263-273) so the behavior won't silently regress unnoticed, but the function itself still returns the wrong answer for empty/empty.

Environment:

  • Deploy: any (pure function, not currently wired into a live route)
  • Commit: 6411ddb

Repro steps:

  1. Call ogrenotes_auth::oauth::verify_state("", "").
  2. Observe it returns true.

Expected: Empty-vs-empty should not be treated as a valid state match — an absent/empty stored state should never successfully "verify" against an absent/empty received state; the function should reject this case (or the call site must reject it first, and that requirement should be made structurally unavoidable rather than relying on caller discipline).

Observed: verify_state("", "") returns true, per the existing test at crates/auth/src/oauth.rs:273, which pins this with a warning comment (line 272) rather than asserting the "safe" behavior.

Evidence:

  • Code: crates/auth/src/oauth.rs:71 (pub fn verify_state)
  • Test (pins current behavior with a warning): crates/auth/src/oauth.rs:263-273
  • Confirmed non-reachability: grep -rn "verify_state" crates/api/ shows no call sites outside crates/auth/src/oauth.rs's own module; the live callback path is crates/api/src/routes/auth.rs:534-560 (handle_callback), which uses state.redis_session.take_oauth_flow(&params.state) and rejects Ok(None) before any comparison.

Suspected area: crates/auth/src/oauth.rs::verify_state — add an explicit rejection for empty/empty (or empty-either-side) inputs so the function is safe by construction rather than relying on every future caller to pre-check for absent state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingverification-findingFinding from a verification run

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions