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.ServeSSO → SessionProvider.GetSession)
POST /login/{shortcut} (HandleIDPInitiated)
Steps to reproduce
- Create a server with
samlidp.New(...) and add a user via PUT /users/alice (with a password).
- Send
POST /login with user=alice&password=wrongpassword and measure the response time (~tens–100 ms — bcrypt runs).
- Send
POST /login with user=ghost&password=wrongpassword for a non-existent user and measure the response time (sub-millisecond — no bcrypt).
- 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
samlidp.Server.GetSessionvalidates login credentials in a way whose response time depends on whether the submitted username exists. A request for a non-existent user returnsimmediately, 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 cantherefore 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 passwordform), so timing is the only distinguisher — but it is a reliable one.When
Store.Getmisses the handler returns beforebcrypt.CompareHashAndPasswordis 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 requiredPOST /sso(IdentityProvider.ServeSSO→SessionProvider.GetSession)POST /login/{shortcut}(HandleIDPInitiated)Steps to reproduce
samlidp.New(...)and add a user viaPUT /users/alice(with a password).POST /loginwithuser=alice&password=wrongpasswordand measure the response time (~tens–100 ms — bcrypt runs).POST /loginwithuser=ghost&password=wrongpasswordfor a non-existent user and measure the response time (sub-millisecond — no bcrypt).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
bcryptcomparison on every attempt — against the user's stored hash when they exist or not exists