Common Login Flow Bugs and How to Catch Them
Common Login Flow Bugs and How to Catch Them
Common Login Flow Bugs and How to Catch Them
Login flows are the gatekeepers of any application. When they break, users are locked out, attackers gain a foothold, and trust erodes quickly. This guide walks through the most frequent login‑flow defects, explains why they appear, shows what they look like to real people, and gives repeatable ways to reproduce, detect, fix, and prevent them. Each pattern includes a concrete example, a snippet you can drop into a test harness, and a note on how persona‑driven autonomous exploration (like the SUSATest platform) surfaces issues that scripted checks often miss.
---
1. Understanding the Login Flow Anatomy
1.1 Core Components
A typical login sequence consists of:
- Entry point – a URL or deep link that renders the login screen.
- Input fields – username/email, password, optional second‑factor code, remember‑me toggle.
- Client‑side validation – JavaScript checks for format, length, empty values.
- Network request – POST to
/auth/login(or similar) with credentials, often JSON or form‑encoded. - Server‑side validation – credential lookup, password hash comparison, account status checks.
- Session creation – issuance of a session cookie, JWT, or token, plus optional remember‑me cookie.
- Post‑login redirect – send the user to a landing page, dashboard, or original target URL.
- Cleanup – clearing temporary tokens, handling MFA challenges, logging audit events.
Each step is a potential failure point. Mis‑configurations, race conditions, or overlooked edge cases can turn a smooth flow into a broken experience.
1.2 Where Bugs Hide
- Input handling – truncation, encoding, or whitespace issues.
- State management – stale tokens, missing invalidation, or cookie scope errors.
- Third‑party integrations – OAuth redirect URIs, SAML Assertion Consumer Service URLs.
- Security controls – rate limiting, lockout policies, CAPTCHA thresholds.
- Accessibility – missing labels, poor contrast, keyboard traps.
Understanding this anatomy lets you map symptoms to root causes quickly.
---
2. Bug Pattern 1: Credential Validation Misconfiguration
2.1 Why It Happens
Developers sometimes rely on client‑side regexes to enforce email or password rules and forget to mirror those rules on the server. When the server accepts a broader set of characters, attackers can inject unexpected payloads (e.g., SQL‑like strings) that bypass business logic or cause downstream parsing errors.
2.2 User Impact
- Users with valid but “unusual” credentials (e.g.,
user+tag@example.com) receive a generic “invalid credentials” message despite being correct. - Attackers may slip through weak server‑side checks and manipulate authentication logic.
2.3 Reproduction Steps
- Identify the accepted character set from the UI (often shown as a tooltip).
- Attempt login with an email containing a plus sign or a sub‑domain with a hyphen.
- Observe whether the request reaches the server and whether the response is a 200 OK with a session cookie or a 400/401 error.
2.4 Detection Techniques
Manual – Use a browser’s developer tools to edit the request payload and resend with varied inputs.
Automated – Add a parameterized test in your unit test suite:
import pytest
import requests
@pytest.mark.parametrize("email", [
"user+test@example.com",
"user.name@sub-domain.example.co.uk",
"user@@example.com", # invalid double @
])
def test_email_acceptance(email):
resp = requests.post(
"https://api.example.com/auth/login",
json={"email": email, "password": "ValidPass!123"},
timeout=5,
)
# Expect 200 for valid formats, 400 for clearly invalid ones
if "@@" in email:
assert resp.status_code == 400
else:
assert resp.status_code == 200
assert "session_id" in resp.cookies
CI Integration – Run the above as part of your nightly security suite; fail the build on unexpected status codes.
2.5 Fix & Prevention
- Mirror client‑side validation on the server using a trusted library (e.g.,
email-validatorfor Python,validator.jsfor Node). - Normalize inputs (trim whitespace, lower‑case domain) before any comparison.
- Add unit tests that cover edge cases: plus addressing, sub‑domains, internationalized domain names (IDN).
- Enforce a schema‑first approach: define the login request JSON schema and validate against it early in the handler.
---
3. Bug Pattern 2: Password Reset Token Leakage
3.1 Why It Happens
Reset tokens are often embedded in URLs sent via email. If the application logs full URLs (including query strings) at DEBUG level, or if the Referer header is forwarded to third‑party analytics, the token can be exposed in logs or external servers.
3.2 User Impact
- An attacker who gains access to server logs or analytics dashboards can hijack accounts.
- Users may notice unauthorized password changes and lose trust in the service.
3.3 Reproduction Steps
- Trigger a password reset for a test account.
- Capture the email and extract the reset link.
- Check server logs (e.g.,
grep reset_token /var/log/app.log) for the token appearing in plain text. - Optionally, configure a mock external site as the Referer and see if the token appears in its request logs.
3.4 Detection Techniques
Manual – Search logs for token strings after a reset request.
Automated – Add a log‑scanning step to your CI pipeline:
#!/usr/bin/env bash
LOG_FILE="/var/log/app.log"
TOKEN_REGEX="reset_token=[A-Za-z0-9\-_]{32,}"
if grep -E "$TOKEN_REGEX" "$LOG_FILE"; then
echo "ERROR: Reset token found in logs"
exit 1
fi
SUSA Mention – When you point SUSATest at a staging URL, its autonomous agents automatically crawl password‑reset flows, capture outbound emails, and verify that no token appears in HTTP request headers or logged responses, flagging any leakage instantly.
3.5 Fix & Prevention
- Never include sensitive tokens in URLs that might be logged; prefer POST bodies or short‑lived one‑time codes stored server‑side.
- Strip query strings from logs at the ingestion point (e.g., configure Logstash to redact
reset_token). - Set
Referrer-Policy: no-referreron authentication endpoints to prevent leakage via Referer. - Rotate tokens immediately after use and enforce short expiry (≤ 15 minutes).
---
4. Bug Pattern 3: Session Fixation after Login
4.1 Why It Happens
If the application accepts a session identifier presented by the user before authentication and does not regenerate it after a successful login, an attacker can fixate a known session ID, trick the victim into logging in, and then hijack the authenticated session.
4.2 User Impact
- Victim logs in successfully but remains unaware that the attacker now shares the same session.
- Post‑login actions (e.g., transferring funds) can be performed by the attacker without additional credentials.
4.3 Reproduction Steps
- Obtain a cookie
SESSIONID=abc123from the login page (before submitting credentials). - Send a link to a test user that forces the cookie (e.g., via a crafted URL
https://app.example.com/login?SESSIONID=abc123). - Have the user log in with their credentials.
- Using the attacker’s browser, send a request with
SESSIONID=abc123to a protected endpoint and verify you receive an authenticated response.
4.4 Detection Techniques
Manual – Use two browsers: one to set a known cookie, another to perform login and then test access with the original cookie.
Automated – Selenium or Playwright script:
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const context1 = await browser.newContext();
await context1.addCookies([{ name: 'SESSIONID', value: 'attacker123', domain: '.example.com', path: '/' }]);
const page1 = await context1.newPage();
await page1.goto('https://app.example.com/login');
// leave page1 idle, attacker holds the cookie
const context2 = await browser.newContext(); // clean session for victim
const page2 = await context2.newPage();
await page2.goto('https://app.example.com/login');
await page2.fill('input[name="email"]', 'victim@example.com');
await page2.fill('input[name="password"]', 'VictimPass!');
await page2.click('button[type="submit"]');
await page2.waitForURL('**/dashboard');
// extract cookies after login
const cookies = await context2.cookies();
const sessionCookie = cookies.find(c => c.name === 'SESSIONID');
console.log('Session after login:', sessionCookie.value);
// try to use attacker's cookie on a protected endpoint
const { request } = await context1.newPage();
const resp = await request.fetch('https://api.example.com/account/settings', {
headers: { cookie: `SESSIONID=attacker123` },
});
console.log('Status with attacker cookie:', resp.status()); // should be 401/403 if fixed
await browser.close();
})();
If the script shows a 200 response with the attacker’s cookie, fixation is present.
4.5 Fix & Prevention
- Always generate a new session ID after successful authentication (
session.regenerate()in most frameworks). - Invalidate any pre‑existing session identifiers associated with the authentication request.
- Set the
SecureandHttpOnlyflags on session cookies, and considerSameSite=Strict. - Add a test that asserts the session ID changes post‑login (as shown above).
---
5. Bug Pattern 4: MFA Bypass via Race Condition
5.1 Why It Happens
Some implementations check the password first, then issue a temporary token for the second factor. If the endpoint that validates the MFA code does not enforce a one‑time use or does not bind the token to the specific login attempt, an attacker can race: submit a valid password, obtain the attacker sends many MFA code guesses while the legitimate user is still entering the code, hoping one guess hits before the token expires.
5.2 User Impact
- Legitimate users may be locked out if the attacker exhausts the token’s attempts.
- Attackers may gain access without knowing the second factor, undermining the purpose of MFA.
5.3 Reproduction Steps
- Initiate a login with correct username and password.
- Capture the temporary MFA token returned (often in a JSON response or a hidden field).
- In a tight loop, send POST requests to
/auth/mfa-verifywith different 6‑digit codes, re‑using the same token. - Observe whether any attempt returns a success response before the token’s intended expiry.
5.4 Detection Techniques
Manual – Use Burp Suite Intruder to brute‑force the MFA code with the same token, monitoring for a 200 OK.
Automated – A concise Node script:
const axios = require('axios');
async function mfaRace(token) {
for (let i = 0; i < 1000000; i++) {
const code = String(i).padStart(6, '0');
try {
const res = await axios.post('https://api.example.com/auth/mfa-verify',
{ token, code },
{ timeout: 2000 }
);
if (res.status === 200) {
console.log(`Success with code ${code}`);
return true;
}
} catch (err) {
// ignore timeouts or 4xx errors, continue looping
}
}
return false;
}
// Example usage (replace with real token from a login step)
mfaRace('tmpToken123').then(ok => console.log('Bypass possible:', ok));
If the loop finds a valid code far before the expected expiry window, a race condition exists.
5.5 Fix & Prevention
- Bind the MFA token to the specific authentication attempt (include a nonce or user‑session ID) and invalidate it after first use, success or failure.
- Enforce strict rate limiting on the MFA verification endpoint (e.g., 5 attempts per token per minute).
- Use cryptographically random, short‑lived tokens (≤ 60 seconds) and store them server‑side, not in the client.
- Add unit tests that attempt to reuse a token after a successful verification and assert a 401/403 response.
---
6. Bug Pattern 5: Remember‑Me Cookie Theft / Insecure Storage
6.1 Why It Happens
Remember‑me tokens are often long‑lived and stored in persistent cookies. If the cookie lacks the Secure flag, is accessible via JavaScript (HttpOnly missing), or is scoped to a overly broad domain, it can be stolen via XSS or network sniffing.
6.2 User Impact
- An attacker who steals the cookie can impersonate the user indefinitely, bypassing password and MFA.
- Users may notice unauthorized sessions appearing in their account activity.
6.3 Reproduction Steps
- Log in with the remember‑me option enabled.
- Inspect the cookie attributes in the browser’s storage pane: verify
Secure,HttpOnly,SameSite, andDomain. - If any flag is missing or the domain is set to a parent domain (e.g.,
.example.comwhen the app lives onapp.example.com), the cookie is vulnerable. - Optionally, deploy a test XSS payload that attempts to read
document.cookieand exfiltrate it to a remote server.
6.4 Detection Techniques
Manual – Use the browser’s developer tools → Application → Cookies to audit each attribute.
Automated – A lightweight Selenium check:
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get("https://app.example.com/login")
# perform login with remember-me checked
driver.find_element(By.NAME, "email").send_keys("user@example.com")
driver.find_element(By.NAME, "password").send_keys("SecurePass!123")
driver.find_element(By.ID, "remember-me").click()
driver.find_element(By.XPATH, "//button[@type='submit']").click()
# grab the remember-me cookie
cookies = driver.get_cookies()
remember = next((c for c in cookies if c['name'] == 'remember_me'), None)
if remember:
print("Cookie:", remember)
issues = []
if not remember['secure']:
issues.append("missing Secure flag")
if not remember['httpOnly']:
issues.append("missing HttpOnly flag")
if remember['sameSite'] not in ('Lax', 'Strict'):
issues.append(f"weak SameSite: {remember['sameSite']}")
if remember['domain'].startswith('.') and remember['domain'] != '.example.com':
issues.append("over‑broad domain")
if issues:
print("Vulnerabilities:", ", ".join(issues))
driver.quit()
If any issue is printed, the remember‑me implementation needs hardening.
6.5 Fix & Prevention
- Set
Secure,HttpOnly, andSameSite=Stricton remember‑me cookies. - Scope the cookie to the exact host (
app.example.com) rather than a parent domain. - Encrypt the token value and store a server‑side hash; rotate the token periodically (e.g., every 30 days).
- Implement re‑authentication for sensitive actions even when a remember‑me cookie is present.
---
7. Bug Pattern 6: Locale / Internationalization Input Validation
7.1 Why It Happens
Applications that support multiple locales often accept username or email inputs in various scripts (Cyrillic, Arabic, accented Latin). If the backend normalizes inputs using ASCII‑only routines or fails to enforce Unicode length limits, attackers can supply Unicode look‑alike characters or excessively long strings that cause buffer overflows, truncation, or unexpected comparison failures.
7.2 User Impact
- Legitimate users with non‑ASCII credentials may be rejected incorrectly.
- Attackers may bypass uniqueness checks (e.g., register
usеr@example.comusing full‑width Latin letters that visually mimicuser@example.combut are stored differently). - Excessively long inputs can lead to denial‑of‑service or injection vectors in downstream services.
7.3 Reproduction Steps
- Attempt to register or log in with an email containing Unicode characters:
üser@exämple.com. - Try a look‑alike attack: replace Latin
awith Cyrillicа(U+0430). - Submit an extremely long local‑part ( > 254 characters ) to see if the system truncates or errors.
- Check whether the account is created, whether login succeeds, and whether any error messages reveal stack traces.
7.4 Detection Techniques
Manual – Use a Unicode test string generator (many online tools) and submit via the UI or API.
Automated – Add a parameterized test in your contract test suite:
@Test
void emailUnicodeHandling() {
String[] emails = {
"üser@exämple.com",
"user@экзампл.ком", // Cyrillic domain
"userⅯ@example.com", // full‑width M
"a".repeat(300) + "@example.com"
};
for (String email : emails) {
Response res = given()
.contentType("application/json")
.body(Map.of("email", email, "password", "ValidPass!123"))
.post("/auth/login");
// Expect either 200 (if allowed) or 400 with a clear validation message
assertTrue(res.statusCode() == 200 || res.statusCode() == 400);
if (res.statusCode() == 400) {
assertTrue(res.body().asString().contains("invalid email"));
}
}
}
If any request returns a 500 error or leaks internal details, the validation is insufficient.
7.5 Fix & Prevention
- Use a robust email validation library that follows the RFC 6531 (internet mail with UTF‑8) standard.
- Enforce a maximum length on the entire email string (254 chars) and on each label (63 chars) before Unicode normalization.
- Normalize to Unicode NFC or NFDC before checking uniqueness or performing comparisons.
- Log validation failures generically (e.g., “invalid email”) without exposing the offending input.
---
8. Bug Pattern 7: Social Login (OAuth) Redirect URI Mismatch
8.1 Why It Happens
When integrating Google, Facebook, Apple, or custom OAuth providers, developers configure a list of allowed redirect URIs. If the application dynamically builds the redirect URL based on request headers (e.g., X-Forwarded-Host) without strict validation, an attacker can manipulate the host to point to a malicious site, steal the authorization code, and exchange it for a token.
8.2 User Impact
- The user is redirected to a attacker‑controlled landing page after granting consent, often without noticing the subtle URL change.
- The attacker receives a valid authorization code and can access the user’s data on the provider (e.
8.3 Reproduction Steps
- Initiate a social login flow (e.g., “Login with Google”).
- Capture the redirect URL request (the browser’s GET to the provider’s authorization endpoint).
- Modify the
redirect_uriquery parameter to point tohttps://attacker.com/callback. - Observe whether the provider accepts the request (many providers will reject if the URI is not pre‑registered, but some misconfigurations allow wildcard or sub‑domain matches).
- If the provider accepts, complete the flow on the attacker’s server and verify you receive an authorization code.
8.4 Detection Techniques
Manual – Use an intercepting proxy (Burp, OWASP ZAP) to tamper with the redirect_uri parameter and see if the provider’s response is an error or a consent screen.
Automated – A simple script that attempts to register a fake redirect URI with a well‑known provider’s sandbox (if available) or checks your own validation logic:
import re
from urllib.parse import urlparse, parse_qs
def is_redirect_allowed(uri):
allowed = [
"https://app.example.com/auth/google/callback",
"https://app.example.com/auth/facebook/callback",
]
# Normalize: lower case, remove default ports, strip trailing slash
norm = uri.lower().rstrip('/')
return norm in allowed
# Example tampered URI
tampered = "https://app.example.com:80/auth/google/callback/../evil.com"
print(is_redirect_allowed(tampered)) # Should be False; if True, vulnerability
If the function returns True for a malformed URI, your validation is too permissive.
8.5 Fix & Prevention
- Maintain a strict allow‑list of redirect URIs and compare using exact string matching after lower‑casing and removing default ports.
- Never rely on headers like
HostorX-Forwarded-Hostto construct the redirect URL; use a configured base URL from your deployment environment. - Enable PKCE (Proof Key for Code Exchange) for public clients to mitigate code interception.
- Write unit tests that feed a variety of malformed URIs (different schemes, ports, paths, query strings) and assert rejection.
---
9. Bug Pattern 8: CAPTCHA Challenges Blocking Legitimate Users
9.1 Why It Happens
Overzealous anti‑abuse mechanisms may present a CAPTCHA after a low number of failed attempts, or they may use a service that fails to load for users with certain browser extensions, ad blockers, or network restrictions.
9.2 User Impact
- Genuine users get stuck at the login page, leading to abandonment and support tickets.
- Accessibility‑focused users (screen‑reader users) may find the CAPTCHA incompletely described, causing a hard block.
9.3 Reproduction Steps
- From a clean browser profile (no extensions, default settings), attempt to log in with incorrect credentials five times in quick succession.
- Observe whether a CAPTCHA widget appears after the expected threshold.
- Repeat the test with a popular ad blocker (e.g., uBlock Origin) enabled; note if the CAPTCHA fails to render or if the challenge audio is inaccessible.
- Test with a screen reader (NVDA or VoiceOver) to verify that the CAPTCHA provides an accessible alternative or adequate ARIA labeling.
9.4 Detection Techniques
Manual – Use the browser’s accessibility inspector to verify that the CAPTCHA iframe has a title and that any audio challenge is keyboard operable.
Automated – Playwright can assert the presence or absence of a CAPTCHA after a defined number of failures:
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://app.example.com/login');
// Simulate 5 failed login attempts
for (let i = 0; i < 5; i++) {
await page.fill('input[name="email"]', `bad${i}@example.com`);
await page.fill('input[name="password"]', 'wrong');
await page.click('button[type="submit"]');
await page.waitForTimeout(800); // wait for feedback
}
// Check if CAPTCHA iframe appears
const captcha = await page.$('iframe[title*="reCAPTCHA"], iframe[src*="hcaptcha"]');
if (captcha) {
console.log('CAPTCHA presented after 5 failures');
// Optionally test audio button
const audioBtn = await page.$('button[aria-label*="audio"]');
if (audioBtn) {
await audioBtn.click();
await page.waitForTimeout(1500);
console.log('Audio challenge triggered');
}
} else {
console.log('No CAPTCHA detected – maybe threshold too high');
}
await browser.close();
})();
If the script reports a CAPTCHA after too few attempts, or if the audio button is missing, the flow may be overly restrictive or inaccessible.
9.5 Fix & Prevention
- Set a sensible failure threshold (e.g., 10 failed attempts) before showing a CAPTCHA, and combine it with IP‑based rate limiting.
- Prefer CAPTCHA providers that offer accessible audio challenges and proper ARIA labels.
- Provide a fallback mechanism (e.g., email‑based verification) for users who cannot solve the visual challenge.
- Log CAPTCHA presentations and successes to tune thresholds over time.
---
10. Bug Pattern 9: Auto‑Fill Interference with Hidden Fields
10.1 Why It Happens
Browsers’ password managers and address auto‑fill can inadvertently populate hidden or non‑visible fields (e.g., a hidden username field used for tracking, or a honeypot field designed to catch bots). When these fields get filled, the submitted payload may violate server‑side expectations, leading to validation errors or silent data corruption.
10.2 User Impact
- Users experience seemingly random login failures despite entering correct credentials.
- Support teams struggle to reproduce the issue because it depends on the specific browser/version and its auto‑fill heuristics.
10.3 Reproduction Steps
- Enable the browser’s built‑in password manager and ensure it has saved credentials for the test site.
- Load the login page and let the auto‑fill run (often triggered on page load or focus).
- Inspect the network request to see if any hidden fields (e.g.,
name="hp"orname="affiliate_id") now contain values. - Submit the form and observe the server response (often a 400 with a cryptic message about unexpected fields).
10.4 Detection Techniques
Manual – Use Chrome DevTools → Settings → Preferences → “Enable auto‑fill” and watch the request payload in the Network tab.
Automated – Selenium can detect whether auto‑fill altered hidden inputs:
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
driver = webdriver.Chrome()
driver.get("https://app.example.com/login")
# Let the page load and auto‑fill run
time.sleep(2)
hidden_inputs = driver.find_elements(By.XPATH, "//input[@type='hidden']")
changed = []
for inp in hidden_inputs:
value = inp.get_attribute("value")
if value and value.strip():
changed.append((inp.get_attribute("name"), value))
if changed:
print("Auto-fill populated hidden fields:", changed)
else:
print("No hidden fields altered by auto-fill")
driver.quit()
If any hidden field shows a non‑empty value, you have a potential interference problem.
10.5 Fix & Prevention
- Rename honeypot fields to names that are unlikely to be guessed by auto‑fill (e.g.,
fp_9f3b2) and addautocomplete="off"orautocomplete="new-password"where appropriate. - Explicitly set
autocomplete="off"on the entire form or on specific fields you wish to protect. - On the server, ignore any fields that are not part of the defined schema; return a 400 if unexpected keys appear, but also log the occurrence for monitoring.
- Provide a visible “Show password” toggle that does not rely on hidden fields for state.
---
11. Bug Pattern 10: Accessibility Failures in Login Form
11.1 Why It Happens
Developers sometimes prioritize visual design over semantic markup. Missing elements, low contrast text, non‑keyboard‑navigable custom widgets, and missing ARIA live regions for error messages all impede users who rely on assistive technologies.
11.2 User Impact
- Screen‑reader users cannot associate inputs with their purpose, leading to guesswork and errors.
- Keyboard‑only users may be trapped in a modal or unable to reach the submit button.
- Low‑contrast text reduces readability for users with visual impairments or in bright environments.
11.3 Reproduction Steps
- Navigate to the login page with a screen reader (NVDA, JAWS, VoiceOver) enabled.
- Tab through the form; note whether each input announces its label.
- Use a contrast analyzer (e.g., Chrome DevTools → Contrast) to verify that text meets WCAG AA (≥ 4.5:1 for normal text).
- Attempt to submit the form with only the keyboard; ensure focus moves logically and that error messages appear in the accessibility tree.
11.4 Detection Techniques
Manual – Run axe‑core or Lighthouse audits; they will flag missing labels, contrast issues, and inaccessible custom controls.
Automated – Integrate axe into your CI pipeline:
npm install -g axe-cli
axe https://app.example.com/login --tags wcag2aa --output json > axe-report.json
# Fail the
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free