[SECURITY AUDIT] Username enumeration via login timing side-channel (bcrypt runs only for existing users) #35

Open
opened 2026-07-04 22:02:02 +00:00 by wtclaude · 0 comments
Member

Severity: Low
Confidence: Medium-High (mechanism is clear; real-world exploitability depends on network jitter and is partly blunted by rate limits)
Scope area: Admin login hardening (audit area 1)

What

The login handler short-circuits the password check when the username does not exist:

// server/src/router/v1/auth/auth.controller.js:51-52
const user = await users.getRawByUsername(username)
const ok = user && (await users.validatePassword(user, password))

Because of &&, bcrypt.compare (users.validatePassword, server/src/model/users/users.model.js:46-49) only runs when the username matches a real account. bcrypt at SALT_ROUNDS = 10 costs on the order of ~100ms; a non-existent username returns almost immediately. That measurable difference lets an attacker enumerate valid admin usernames despite the (good) generic Incorrect username or password. message. The same pattern exists in the mobile login path (server/src/router/v1/auth/mobile.controller.js:56-57).

Why it matters

The response body is correctly uniform, but the timing is not, defeating the intent of the generic message. Knowing a valid admin username narrows a subsequent credential-stuffing/brute-force campaign. Impact is limited because per-IP backoff, slow-down, and the 10/15-min cap raise the cost of sampling — hence Low — but the enumeration oracle itself is real.

Where

  • server/src/router/v1/auth/auth.controller.js:51-52
  • server/src/router/v1/auth/mobile.controller.js:56-57
  • server/src/model/users/users.model.js:46-49 (validatePassword returns early on missing hash)

Suggested fix (not implemented)

Always perform a bcrypt comparison against a fixed dummy hash when the user is not found, so both branches take comparable time (a "compare against a constant hash" dummy-work pattern). Keep the generic response unchanged. This equalizes the timing without leaking which factor failed.

**Severity:** Low **Confidence:** Medium-High (mechanism is clear; real-world exploitability depends on network jitter and is partly blunted by rate limits) **Scope area:** Admin login hardening (audit area 1) ### What The login handler short-circuits the password check when the username does not exist: ```js // server/src/router/v1/auth/auth.controller.js:51-52 const user = await users.getRawByUsername(username) const ok = user && (await users.validatePassword(user, password)) ``` Because of `&&`, `bcrypt.compare` (`users.validatePassword`, `server/src/model/users/users.model.js:46-49`) only runs when the username matches a real account. bcrypt at `SALT_ROUNDS = 10` costs on the order of ~100ms; a non-existent username returns almost immediately. That measurable difference lets an attacker enumerate valid admin usernames despite the (good) generic `Incorrect username or password.` message. The same pattern exists in the mobile login path (`server/src/router/v1/auth/mobile.controller.js:56-57`). ### Why it matters The response body is correctly uniform, but the timing is not, defeating the intent of the generic message. Knowing a valid admin username narrows a subsequent credential-stuffing/brute-force campaign. Impact is limited because per-IP backoff, slow-down, and the 10/15-min cap raise the cost of sampling — hence Low — but the enumeration oracle itself is real. ### Where - `server/src/router/v1/auth/auth.controller.js:51-52` - `server/src/router/v1/auth/mobile.controller.js:56-57` - `server/src/model/users/users.model.js:46-49` (`validatePassword` returns early on missing hash) ### Suggested fix (not implemented) Always perform a bcrypt comparison against a fixed dummy hash when the user is not found, so both branches take comparable time (a "compare against a constant hash" dummy-work pattern). Keep the generic response unchanged. This equalizes the timing without leaking which factor failed.
wtclaude added the
severity:low
label 2026-07-04 22:02:02 +00:00
Sign in to join this conversation.
No description provided.