Skip to content

Sign the OIDC login flow-state cookie (login CSRF / session fixation)#1312

Closed
lovasoa wants to merge 1 commit into
mainfrom
ophir.lojkine/fix-oidc-callback-state-fixation
Closed

Sign the OIDC login flow-state cookie (login CSRF / session fixation)#1312
lovasoa wants to merge 1 commit into
mainfrom
ophir.lojkine/fix-oidc-callback-state-fixation

Conversation

@lovasoa

@lovasoa lovasoa commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Sign the OIDC login flow-state cookie (login CSRF / session fixation)

The built-in OIDC login flow stored its temporary state in an unsigned JSON cookie named after the state parameter (sqlpage_oidc_state_<state>), and the callback trusted whatever cookie matched the callback's state (oidc.rs:1107 stores unsigned JSON, oidc.rs:703 selects it by state, oidc.rs:716 sets sqlpage_auth).

An attacker who can plant a cookie for the SQLPage origin (or a parent domain) primes a state + matching sqlpage_oidc_state_<state> cookie plus an authorization code for an identity they control. A victim hitting /sqlpage/oidc_callback?code=<attacker_code>&state=<attacker_state> then gets set-cookie: sqlpage_auth=<JWT for the attacker identity>: login CSRF / session fixation, logging the victim into the attacker's account. (Not token forgery; precondition is cookie-planting.)

Fix

HMAC-SHA256-sign the cookie value with the OIDC client secret (the same key material as the existing compute_logout_signature) and verify it on the callback. A login-state cookie the server never produced is rejected.

Proof

New integration test test_oidc_forged_state_cookie_is_rejected drives the callback with an attacker-planted (unsigned) state cookie and asserts no sqlpage_auth is set.

Before the fix it fails: the forged cookie produces a login.

thread 'oidc::test_oidc_forged_state_cookie_is_rejected' panicked at tests/oidc/mod.rs:466:5:
Forged (un-signed) flow-state cookie must not result in a login. Got cookies: [Cookie { cookie_string: Some("sqlpage_auth=eyJhbG...; ... "), ... }, ...]

After the fix, the full OIDC suite passes, including the existing happy-path login:

$ cargo test --test mod oidc
running 12 tests
test oidc::test_oidc_forged_state_cookie_is_rejected ... ok
test oidc::test_oidc_happy_path ... ok
...
test result: ok. 12 passed; 0 failed

cargo clippy --all-targets --all-features -- -D warnings is clean. A focused unit test (login_flow_state_cookie_round_trips_and_rejects_forgery) also covers sign/verify, forged-signature rejection, and wrong-secret rejection.

In-flight logins started before the upgrade simply restart the OIDC flow; no config change needed.

…on fixation

The temporary OIDC login-state cookie (sqlpage_oidc_state_<state>) was stored
as unsigned JSON and trusted on the callback purely by its name matching the
callback's state parameter. An attacker able to plant a cookie for the SQLPage
origin could pre-seed flow state the server never issued and, together with an
authorization code for an identity they control, log a victim's browser into
that attacker-controlled identity.

Sign the cookie value with HMAC-SHA256 keyed by the OIDC client secret (same
key material as compute_logout_signature) and verify it on the callback,
rejecting any login-state cookie this server did not produce.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 15d16a0357

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/webserver/oidc.rs
Comment on lines +1120 to +1121
let signature = compute_login_flow_state_signature(&payload, client_secret);
let cookie_value = format!("{payload}.{signature}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind signed flow state to the initiating browser

Signing the payload does not prevent the documented login-CSRF scenario because an attacker can initiate a legitimate login flow in their own browser, copy the resulting server-signed flow-state cookie, complete authorization for their identity, and then plant that exact cookie in the victim's browser before directing it to the callback. Since the HMAC only proves that the server issued the payload and is not bound to the initiating browser or stored as one-time server-side state, verification succeeds and the victim is still logged into the attacker's identity.

Useful? React with 👍 / 👎.

@lovasoa

lovasoa commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as out of scope under SQLPage's threat model.

This login-CSRF/session-fixation requires the attacker to plant a cookie on the SQLPage origin, which is the precondition the original report stated. The HMAC-signing approach in this PR does not actually close it: an attacker can start their own legitimate flow, copy the validly server-signed state cookie, and plant it. A server-side one-time state store does not close it either, since the attacker's state is validly issued and the victim consumes it once (that only prevents replay). The effective mitigation would be cookie-injection hardening (e.g. a __Host- prefix), but defending against an attacker who can already write cookies to the exact origin is out of scope here.

@lovasoa lovasoa closed this Jun 10, 2026
@lovasoa lovasoa deleted the ophir.lojkine/fix-oidc-callback-state-fixation branch June 10, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant