Skip to content

samlidp: login is not constant-time — response timing reveals whether a username exists (user enumeration) #665

Description

@avinashdhanshetty

samlidp.Server.GetSession validates login credentials in a way whose response time depends on whether the submitted username exists. A request for a non-existent user returns
immediately, while a request for an existing user with a wrong password runs bcrypt.CompareHashAndPassword (tens to ~100 ms at the default cost). An unauthenticated attacker can
therefore enumerate valid usernames/emails by timing the responses.

The response body and status are already identical for both cases (both render the same Invalid username or password form), so timing is the only distinguisher — but it is a reliable one.

When Store.Get misses the handler returns before bcrypt.CompareHashAndPassword is ever called. Only an existing user pays the bcrypt cost, creating the timing side channel.

This is reachable through every interactive-login entry point, since all of them funnel into GetSession:

  • POST /login (HandleLogin) — no SAML context required
  • POST /sso (IdentityProvider.ServeSSOSessionProvider.GetSession)
  • POST /login/{shortcut} (HandleIDPInitiated)

Steps to reproduce

  1. Create a server with samlidp.New(...) and add a user via PUT /users/alice (with a password).
  2. Send POST /login with user=alice&password=wrongpassword and measure the response time (~tens–100 ms — bcrypt runs).
  3. Send POST /login with user=ghost&password=wrongpassword for a non-existent user and measure the response time (sub-millisecond — no bcrypt).
  4. The consistent, large difference reveals which usernames exist.

Expected behavior

Login response time should be independent of whether the submitted username exists, so username existence cannot be inferred by timing.

Actual behavior

Non-existent users return before any password hashing; existing users incur the full bcrypt cost. The timing gap discloses account existence.

Suggested fix

Perform exactly one bcrypt comparison on every attempt — against the user's stored hash when they exist or not exists

Activity

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions