Skip to content

sec(auth): login endpoint leaks MFA enrollment status via distinct error messages #388

Description

@cristim

Summary

The /api/auth/login handler passes err.Error() verbatim to the client (HTTP 401). The verifyPasswordAndMFA function returns semantically distinct error messages depending on MFA state, which allows an attacker to confirm both account existence and MFA enrollment:

Condition Error returned to client
Wrong email or password, user not found invalid email or password
Correct email + password, MFA enabled, no code submitted MFA code required
Correct email + password, MFA flag set but secret missing MFA is enabled but not configured

A network attacker who can attempt credentials can confirm whether an account exists and has MFA enabled by observing which 401 message is returned.

Location

  • internal/api/handler_auth.go:40 — NewClientError(401, err.Error())
  • internal/auth/service.go:182–191 — verifyPasswordAndMFA distinct MFA error strings

Reproduction

# Attempt login with correct credentials for a user with MFA
curl -X POST https://<lambda-url>/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"known-user@example.com","password":"correctpassword"}'
# → {"error":"MFA code required"}  ← confirms user exists and MFA is enrolled

Note: live probing was not done for this finding due to the rate limiter. The finding is code-only.

Suggested fix

Map all authentication failures to a single generic message at the handler layer, regardless of internal cause. The MFA-specific messages are useful in the service layer for logging, but should not be forwarded to callers.

// handler_auth.go login()
response, err := h.auth.Login(ctx, loginReq)
if err != nil {
    // Map MFA-specific messages to a generic 401 to prevent user enumeration.
    // The internal error is already logged at the service layer.
    return nil, NewClientError(401, "invalid email or password")
}

The "MFA code required" flow needs a different UX signal — consider returning a structured body like {"error":"authentication_failed","mfa_required":true} only after the MFA-requiring path, or use a dedicated 2-step flow so MFA prompting doesn't confirm credentials.

Severity rationale

MEDIUM: Requires valid credentials to trigger the MFA-specific path (not a pure enumeration oracle — you need the password too), but it does confirm account existence + MFA enrollment state for credential-stuffing actors who managed to obtain matching passwords from a breach.

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

    bugSomething isn't workingeffort/sHoursimpact/fewLimited audiencepr-createdA PR has been opened for this issue (dedup guard for the auto-PR loop)pr-mergedThe PR for this issue has been mergedpriority/p2Backlog-worthyseverity/mediumModerate harmtriagedItem has been triagedtype/securitySecurity findingurgency/this-quarterWithin the quarter

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions