You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[BUG] Username enumeration possible if lockout enabled #240
The /users/_login endpoint reveals whether a username exists in the system by returning different error messages and statuses based on the account's state. Specifically, when password retry limits are enabled, the API response for a valid username includes a "login attempts remaining" counter or a FAIL_LOCKED_OUT status. A non-existent username returns a generic failure. This allows an attacker to distinguish between valid and invalid usernames (Account Enumeration).
To Reproduce
Enable account lockout features in Mirth (e.g., set _MP_PASSWORD_RETRYLIMIT to 5).
Attempt a login with a non-existent username (e.g., notARealUser).
Response: Returns generic FAIL with "Incorrect username or password."
Attempt a login with a known valid username (e.g., admin) and an incorrect password.
Response: Returns FAIL but includes specific metadata: "2 login attempt(s) remaining for "admin"..."
Continue failed attempts on the valid username until the limit is reached.
Response: Returns FAIL_LOCKED_OUT and a message confirming the account is locked for a specific duration.
Expected behavior
To prevent account enumeration, the server should return an identical response (Status Code, JSON Body, and Response Timing) regardless of whether the username exists, the password is incorrect, or the account is currently locked.
The recommended response is a generic: {"status": "FAIL", "message": "Invalid username or password"}
Actual behavior
The API discloses the existence of valid accounts through specific error strings and the LoginStatus enum:
Locked Account: Discloses FAIL_LOCKED_OUT and the time remaining.
Invalid User: Returns a generic failure with no attempt counter.
Workaround(s)
Disable Lockout/Retry Limits: Reverting to default settings (where these are off) stops the specific counter disclosure, though it increases the risk of brute-force success.
Network Restrictions: Do not expose the Mirth API/Administrator port to the public internet; restrict access to a management VPN or trusted IP range. Use a WAF or other security appliance to limit username enumeration.
External Auth: Use a federated identity provider so Mirth is not handling the credential validation logic.
Nonexistent user → bare "Incorrect username or password." LoginRequirementsChecker only gets built when the user exists (:309), so strikes never accrue and there's no counter.
Possible fix: collapse all three to one generic FAIL + "Invalid username or password".
This doesn't cost us anything on the audit side. UserServlet.login already logs a ServerEvent on every attempt (:125) with the attempted username, source IP, and SUCCESS/FAILURE outcome, and strikes/lockout stay tracked server-side. So the operator still sees who tried what; only the caller's response goes generic.
Two things a message-only change won't catch:
Timing. A real username runs digester.matches; a nonexistent one skips it and returns fast, so response time still separates them. Run a dummy digest on the no-user path to even it out.
Lockout itself leaks. A real account locks after N tries; a fake one never does — same message, different behavior, still tells you the account exists. And per-account lockout is a DoS lever: an attacker can deliberately lock out a real admin. NIST 800-63B leans toward rate-limiting/throttling over hard lockout for exactly these reasons. That's a bigger change, probably its own issue.
Tradeoff either way: a locked-out admin stops getting the "try again in N minutes" message. Live with it, or send it out-of-band (e.g. email the account owner).
Confirmed in DefaultUserController.login. Three tells when lockout is enabled:
Possible fix: collapse all three to one generic FAIL + "Invalid username or password".
Activity
Fix information disclosure when lockout enabled
Fix information disclosure when lockout enabled
Fix information disclosure when lockout enabled
Fix information disclosure when lockout enabled
pacmano1 commentedon Aug 10, 2026
Confirmed in
DefaultUserController.login. Three tells when lockout is enabled:FAIL_LOCKED_OUT, returned before the password check (:311).:405).LoginRequirementsCheckeronly gets built when the user exists (:309), so strikes never accrue and there's no counter.Possible fix: collapse all three to one generic
FAIL+ "Invalid username or password".This doesn't cost us anything on the audit side.
UserServlet.loginalready logs aServerEventon every attempt (:125) with the attempted username, source IP, and SUCCESS/FAILURE outcome, and strikes/lockout stay tracked server-side. So the operator still sees who tried what; only the caller's response goes generic.Two things a message-only change won't catch:
digester.matches; a nonexistent one skips it and returns fast, so response time still separates them. Run a dummy digest on the no-user path to even it out.Tradeoff either way: a locked-out admin stops getting the "try again in N minutes" message. Live with it, or send it out-of-band (e.g. email the account owner).
mgaffigan commentedon Aug 10, 2026
Related: #241