Common Login Flow Bugs and How to Catch Them

Common Login Flow Bugs and How to Catch Them

May 12, 2026 · 16 min read · Common Issues

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:

  1. Entry point – a URL or deep link that renders the login screen.
  2. Input fields – username/email, password, optional second‑factor code, remember‑me toggle.
  3. Client‑side validation – JavaScript checks for format, length, empty values.
  4. Network request – POST to /auth/login (or similar) with credentials, often JSON or form‑encoded.
  5. Server‑side validation – credential lookup, password hash comparison, account status checks.
  6. Session creation – issuance of a session cookie, JWT, or token, plus optional remember‑me cookie.
  7. Post‑login redirect – send the user to a landing page, dashboard, or original target URL.
  8. 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

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

2.3 Reproduction Steps

  1. Identify the accepted character set from the UI (often shown as a tooltip).
  2. Attempt login with an email containing a plus sign or a sub‑domain with a hyphen.
  3. 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

---

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

3.3 Reproduction Steps

  1. Trigger a password reset for a test account.
  2. Capture the email and extract the reset link.
  3. Check server logs (e.g., grep reset_token /var/log/app.log) for the token appearing in plain text.
  4. 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

---

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

4.3 Reproduction Steps

  1. Obtain a cookie SESSIONID=abc123 from the login page (before submitting credentials).
  2. Send a link to a test user that forces the cookie (e.g., via a crafted URL https://app.example.com/login?SESSIONID=abc123).
  3. Have the user log in with their credentials.
  4. Using the attacker’s browser, send a request with SESSIONID=abc123 to 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

---

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

5.3 Reproduction Steps

  1. Initiate a login with correct username and password.
  2. Capture the temporary MFA token returned (often in a JSON response or a hidden field).
  3. In a tight loop, send POST requests to /auth/mfa-verify with different 6‑digit codes, re‑using the same token.
  4. 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

---

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

6.3 Reproduction Steps

  1. Log in with the remember‑me option enabled.
  2. Inspect the cookie attributes in the browser’s storage pane: verify Secure, HttpOnly, SameSite, and Domain.
  3. If any flag is missing or the domain is set to a parent domain (e.g., .example.com when the app lives on app.example.com), the cookie is vulnerable.
  4. Optionally, deploy a test XSS payload that attempts to read document.cookie and 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

---

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

7.3 Reproduction Steps

  1. Attempt to register or log in with an email containing Unicode characters: üser@exämple.com.
  2. Try a look‑alike attack: replace Latin a with Cyrillic а (U+0430).
  3. Submit an extremely long local‑part ( > 254 characters ) to see if the system truncates or errors.
  4. 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

---

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

8.3 Reproduction Steps

  1. Initiate a social login flow (e.g., “Login with Google”).
  2. Capture the redirect URL request (the browser’s GET to the provider’s authorization endpoint).
  3. Modify the redirect_uri query parameter to point to https://attacker.com/callback.
  4. 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).
  5. 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

---

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

9.3 Reproduction Steps

  1. From a clean browser profile (no extensions, default settings), attempt to log in with incorrect credentials five times in quick succession.
  2. Observe whether a CAPTCHA widget appears after the expected threshold.
  3. 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.
  4. 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

---

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

10.3 Reproduction Steps

  1. Enable the browser’s built‑in password manager and ensure it has saved credentials for the test site.
  2. Load the login page and let the auto‑fill run (often triggered on page load or focus).
  3. Inspect the network request to see if any hidden fields (e.g., name="hp" or name="affiliate_id") now contain values.
  4. 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

---

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

11.3 Reproduction Steps

  1. Navigate to the login page with a screen reader (NVDA, JAWS, VoiceOver) enabled.
  2. Tab through the form; note whether each input announces its label.
  3. Use a contrast analyzer (e.g., Chrome DevTools → Contrast) to verify that text meets WCAG AA (≥ 4.5:1 for normal text).
  4. 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