• Agents
  • Pricing
  • Blog
Log in
Get started

Security for apps built with AI. Paste a URL, get a report, fix what matters.

Product

  • How it works
  • What we find
  • Pricing
  • Agents
  • MCP Server
  • CLI
  • GitHub Action

Resources

  • Guides
  • Blog
  • Docs
  • OWASP Top 10
  • Glossary
  • FAQ

Security

  • Supabase Security
  • Next.js Security
  • Lovable Security
  • Cursor Security
  • Bolt Security

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • Imprint
© 2026 Flowpatrol. All rights reserved.
Home/OWASP Top 10/Web Top 10/A07: Identification and Authentication Failures
A07CWE-287CWE-297CWE-384CWE-521

Login that lets anybody in
Identification and Authentication Failures

The bug where the sign-in flow works perfectly for you and also works perfectly for the attacker.

One of the most common categories in tested apps.

Reference: Web Top 10 (2021) — A07·Last updated April 7, 2026·By Flowpatrol Team
Identification and Authentication Failures illustration

Auth is the one thing the model always writes. Sign up, sign in, reset password — it never forgets the form. What it forgets is the hundred quiet rules underneath that decide whether the form is a door or a wall. The door works. That is not the same thing as the door being locked.

Identification and authentication failures cover every way a login flow can be technically correct but practically broken. Missing rate limits, weak password rules, predictable reset tokens, session IDs that never expire, MFA that is optional and easy to skip. The form works. The guarantees behind the form do not.

What your AI actually built

You asked for login with email and password. The model built the route, the form, the session cookie, and the redirect. Every happy-path test passes. You tried it yourself and it let you in.

What it didn't build was rate limiting on the login endpoint, so an attacker can try a hundred thousand passwords a minute. No lockout, no captcha, no delay. The form works exactly the same on attempt one and attempt one million.

The JWT it hands out is signed, but the secret is 'secret' in an .env.example that got committed. The password reset token is a random number from Math.random. None of these are typos — they are all defaults the model copied from a tutorial that skipped security for readability.

How it gets exploited

An attacker sees your login page. They have a list of 10 million known breached email/password pairs from a public dump.

  • 1
    Credential stuffing
    They point a simple script at /api/auth/login and replay the breached pairs. No rate limit, no captcha, no lockout — the server answers every request instantly.
  • 2
    Harvest the hits
    Roughly 1 in 1,000 pairs succeeds, because users reuse passwords. In an hour they have 10,000 valid sessions on your app.
  • 3
    Skip MFA
    MFA is optional and most users never enabled it. The ones who did get skipped. The ones who did not get logged in.
  • 4
    Move in quietly
    The logins look legitimate — correct password, normal user agent, spread across IPs via a proxy pool. Nothing trips an alert because there is no alert.
  • Thousands of real user accounts taken over in a single overnight run, using credentials the attacker already had. No exploit was needed — the login form was the exploit.

    Vulnerable vs Fixed

    Vulnerable — no rate limit, weak session
    // app/api/auth/login/route.ts
    export async function POST(req) {
      const { email, password } = await req.json();
    
      const user = await db.user.findUnique({ where: { email } });
      if (!user || !(await bcrypt.compare(password, user.hash))) {
        return Response.json({ error: 'Invalid' }, { status: 401 });
      }
    
      // Same response time, same error, no throttle.
      const token = jwt.sign({ id: user.id }, 'secret');
      return Response.json({ token });
    }
    Fixed — throttled and fenced
    // app/api/auth/login/route.ts
    export async function POST(req) {
      const ip = req.headers.get('x-forwarded-for') ?? 'unknown';
      const { email, password } = await req.json();
    
      // Throttle per IP and per email. Fail closed.
      if (await rateLimit.exceeded(ip, email)) {
        return Response.json({ error: 'Too many attempts' }, { status: 429 });
      }
    
      const user = await db.user.findUnique({ where: { email } });
      const ok = user && (await bcrypt.compare(password, user.hash));
      if (!ok) return Response.json({ error: 'Invalid' }, { status: 401 });
    
      const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET!, {
        expiresIn: '1h',
      });
      return Response.json({ token });
    }

    Two additions — a real rate limiter keyed on both IP and email, and a JWT secret loaded from env with a real expiry. Credential stuffing dies at the first check. The rest of the flow is unchanged.

    A real case

    23andMe credential stuffing leaked 6.9 million profiles

    Attackers replayed breached passwords against 23andMe logins with no throttle, then used the DNA Relatives feature to pivot from each compromised account to thousands of relatives.

    Related reading

    Glossary

    Authentication Failures (Broken Authentication)JSON Web Token Security (JWT Misconfiguration)OAuth 2.0 Implementation Flaws (OAuth Misconfiguration)Brute Force Protection (Rate Limiting)

    What we find

    auth session flaws

    References

    • A07: Identification and Authentication Failures — official OWASP entry
    • OWASP Top 10 for Web Applications (2021) — full list
    • CWE-287 on cwe.mitre.org
    • CWE-297 on cwe.mitre.org
    • CWE-384 on cwe.mitre.org
    • CWE-521 on cwe.mitre.org

    Prove your login actually locks people out.

    Flowpatrol tests every auth flow the way an attacker would and reports the ones that let too much through. Five minutes. One URL.

    Try it free