Skip to content

OIDC state cookies accumulate and can exceed proxy header limits #1237

@lovasoa

Description

@lovasoa

Summary

OIDC login can leave behind an unbounded number of sqlpage_oidc_state_<csrf> cookies. When a browser triggers several unauthenticated protected requests before the login flow completes, or when the OIDC callback fails and retries, SQLPage creates a new state cookie for each redirect attempt but only removes the single cookie referenced by the successful callback. The remaining state cookies continue to be sent on every request until they expire, which can push the request header over reverse-proxy limits and break authentication.

This matches production failures such as:

client sent too long header line: "Cookie: sqlpage_auth=...; sqlpage_oidc_nonce=...; sqlpage_oidc_state_Yr7ZUpz_4bMFLblLH66vyg=...; sqlpage_oidc_state_MhUHU_XlZyBXb2UVWxSTIQ=...; sqlpage_oidc_state_pEOeNNonvws5gwVdpynZOA=..."

Impact

  • Users can get stuck in an authentication failure state because nginx rejects the request before SQLPage sees it.
  • The problem is amplified by large ID tokens because sqlpage_auth already consumes several KB.
  • A single page load can trigger multiple protected XHR/AJAX requests, so cookie growth can happen quickly.
  • Logout does not clean up leftover sqlpage_oidc_state_* cookies, so stale cookies can survive until expiry.

Root Cause

The current OIDC flow creates a distinct temporary state cookie for every redirect attempt:

  • handle_unauthenticated_request always calls build_auth_provider_redirect_response for protected unauthenticated requests. src/webserver/oidc.rs:456
  • build_auth_provider_redirect_response always creates a fresh sqlpage_oidc_state_<csrf> cookie. src/webserver/oidc.rs:737
  • On success, process_oidc_callback removes only the cookie matching params.state. src/webserver/oidc.rs:670 and src/webserver/oidc.rs:691
  • On callback error, handle_oidc_callback_error immediately creates yet another state cookie and does not remove the failed one first. src/webserver/oidc.rs:475
  • On logout, only sqlpage_auth and sqlpage_oidc_nonce are cleared; temporary sqlpage_oidc_state_* cookies are not. src/webserver/oidc.rs:536

This is likely a regression from a451c7e7, which switched from a single shared state cookie to one cookie per CSRF token. That fixed concurrent-login overwrites, but it also made cleanup incomplete.

Reproduction

  1. Configure OIDC protection on a page that triggers multiple protected subrequests or AJAX calls while unauthenticated.
  2. Open the page in a fresh session so several requests race into handle_unauthenticated_request before the browser finishes the redirect.
  3. Complete a single successful login.
  4. Inspect browser cookies or the incoming Cookie header.

Actual Behavior

  • Several sqlpage_oidc_state_<random> cookies remain after login.
  • Future requests include all of them, plus sqlpage_auth and sqlpage_oidc_nonce.
  • Repeated login retries or failed callbacks add more cookies.
  • Eventually nginx rejects requests with client sent too long header line.

Expected Behavior

Temporary OIDC state cookies should be bounded and aggressively cleaned up. After a successful login or logout, the browser should not keep unrelated stale sqlpage_oidc_state_* cookies.

Suggested Fix Direction

One of these approaches should work:

  1. Before setting a new sqlpage_oidc_state_* cookie, clear all existing cookies with the sqlpage_oidc_state_ prefix.
  2. On successful callback and logout, iterate over request cookies and remove every sqlpage_oidc_state_* cookie, not just the matching one.
  3. On callback error, remove the failed state cookie before issuing a new redirect.
  4. Add a regression test covering multiple concurrent unauthenticated requests and verifying that only bounded temporary cookies remain after login/logout.

Notes

The current LOGIN_FLOW_STATE_COOKIE_EXPIRATION is only 10 minutes, but that is still long enough to break users behind strict proxy header limits, especially with large OIDC ID tokens.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions