July 19, 2026
Authentication Bypass: Breaking the Login Itself
A hands-on walkthrough of six real ways attackers bypass authentication brute forcing, rate-limit evasion, SQL injection, MFA flaws…
By Khushi
10 min read
A hands-on walkthrough of six real ways attackers bypass authentication brute forcing, rate-limit evasion, SQL injection, MFA flaws, session hijacking, and JWT tampering with code, exploitation steps, and fixes.
What Is Authentication?
Before breaking logins, let's define what we're breaking.
Authentication is the process of verifying the credentials of a user or device attempting to access a restricted system.
Put simply: Authentication answers "who are you?"
It's the gatekeeper step proving identity via a password, a biometric, a token, or some combination of factors before the system decides what that identity is allowed to do (that second part is authorization, covered in the companion post on Broken Access Control).
Authentication failures happen when that gatekeeping breaks down: weak login protections, flawed session handling, or logic errors that let an attacker prove an identity they don't actually have or skip proving it altogether.
Where It Ranks: OWASP Top 10
The OWASP Top 10 ranks web application risks using real-world data frequency, exploitability, and impact collected across thousands of applications. Identification and Authentication Failures sit at #7.
While Broken Access Control (#1) is about what a user can do once they're in, authentication failures are about getting in in the first place and together, these two categories are behind the vast majority of account takeovers reported in real breaches.
This post walks through six escalating techniques, each one bypassing the "fix" that was supposed to stop the one before it.
1. Brute Forcing & Username Enumeration
What It Is
A brute-force attack is a trial-and-error method used to crack passwords, PINs, or keys an automated script or tool systematically tries every possible combination until it lands on the correct one.
How It Works
The first crack in the door is often username enumeration the login form gives away, through subtle response differences, whether a submitted username actually exists:
user: "admin" → "Invalid password"
user: "xk29zq" → "Invalid username"user: "admin" → "Invalid password"
user: "xk29zq" → "Invalid username"That difference alone tells an attacker which usernames are real, narrowing the attack from "guess everything" to "guess the password for this one confirmed account." From there, tools like Burp Intruder automate password guessing at scale cycling through wordlists in seconds.
Prevention
- Generic error messages ("invalid username or password") regardless of which part was actually wrong
- Account lockout / exponential backoff after repeated failed attempts
- CAPTCHA after a threshold of failures
- Rate limiting (see next section implementation matters more than you'd think)
Practice It
- PortSwigger Lab: Username enumeration via different responses
2. Rate Limit Bypass
The "Fix" That Isn't Enough
Rate limiting controls how often a user can perform an action like a login attempt within a set time period. Sounds like a complete fix for brute forcing. It isn't, if implemented naively.
The Vulnerable Version
const loginAttempts = {}; // tracks attempts by IP only
app.post('/login', (req, res) => {
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
// VULNERABLE: trusts a single, client-controllable header
loginAttempts[ip] = (loginAttempts[ip] || 0) + 1;
if (loginAttempts[ip] > 5) {
return res.status(429).send("Too many attempts. Try again later.");
}
// ...proceed with login check
});const loginAttempts = {}; // tracks attempts by IP only
app.post('/login', (req, res) => {
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
// VULNERABLE: trusts a single, client-controllable header
loginAttempts[ip] = (loginAttempts[ip] || 0) + 1;
if (loginAttempts[ip] > 5) {
return res.status(429).send("Too many attempts. Try again later.");
}
// ...proceed with login check
});How It Breaks
X-Forwarded-For is a header the client can set themselves. An attacker just sends a different fake value with every request:
X-Forwarded-For: 1.1.1.1
X-Forwarded-For: 2.2.2.2
X-Forwarded-For: 3.3.3.3X-Forwarded-For: 1.1.1.1
X-Forwarded-For: 2.2.2.2
X-Forwarded-For: 3.3.3.3Each request looks like a "new" IP to the limiter, so the counter never crosses the threshold. Brute forcing continues, completely unrestricted. This is the exact flaw in picoCTF's "Crack the Gate 2" challenge and in production, attackers use the same trick by rotating other single trust signals too (User-Agent, cookies, request timing).
The Correct Version
const attempts = {}; // { username: { count, lastAttempt } }
app.post('/login', (req, res) => {
const { username, password } = req.body;
const record = attempts[username] || { count: 0, lastAttempt: 0 };
// Lock account after 5 failed attempts within 15 minutes
if (record.count >= 5 && Date.now() - record.lastAttempt < 15 * 60 * 1000) {
return res.status(429).send("Too many attempts. Try again later.");
}
const isValid = checkCredentials(username, password); // parameterized query
if (!isValid) {
attempts[username] = { count: record.count + 1, lastAttempt: Date.now() };
return res.status(401).send("Invalid credentials");
}
attempts[username] = { count: 0, lastAttempt: 0 }; // reset on success
res.send("Login successful");
});const attempts = {}; // { username: { count, lastAttempt } }
app.post('/login', (req, res) => {
const { username, password } = req.body;
const record = attempts[username] || { count: 0, lastAttempt: 0 };
// Lock account after 5 failed attempts within 15 minutes
if (record.count >= 5 && Date.now() - record.lastAttempt < 15 * 60 * 1000) {
return res.status(429).send("Too many attempts. Try again later.");
}
const isValid = checkCredentials(username, password); // parameterized query
if (!isValid) {
attempts[username] = { count: record.count + 1, lastAttempt: Date.now() };
return res.status(401).send("Invalid credentials");
}
attempts[username] = { count: 0, lastAttempt: 0 }; // reset on success
res.send("Login successful");
});The key difference: this version tracks attempts by username, not by a header the attacker can freely rotate. Even if they spoof their IP on every request, the counter tied to the account doesn't reset.
Key takeaway:_ Never rate-limit based on something the attacker can freely change. Combine a network-level signal (real IP) with an identity-level signal (account) defeating one shouldn't defeat the whole defense._
Practice It
- CyLab Security Academy: "Crack the Gate 2"
3. SQL Injection — Login Bypass
What It Is
SQL injection is the insertion of malicious SQL via input data from the client into the application's database query. A successful exploit can read or modify database data, execute admin operations, or in the worst cases, reach the underlying host.
The Vulnerable Code
app.post('/login', (req, res) => {
const { username, password } = req.body;
// VULNERABLE: user input directly concatenated into SQL
const query = `SELECT * FROM users WHERE username='${username}'
AND password='${password}'`;
db.query(query, (err, results) => {
if (results.length > 0) res.send("Login successful");
else res.send("Invalid credentials");
});
});app.post('/login', (req, res) => {
const { username, password } = req.body;
// VULNERABLE: user input directly concatenated into SQL
const query = `SELECT * FROM users WHERE username='${username}'
AND password='${password}'`;
db.query(query, (err, results) => {
if (results.length > 0) res.send("Login successful");
else res.send("Invalid credentials");
});
});How It Gets Injected
An attacker enters this in the username field:
admin' --admin' --The resulting query becomes:
SELECT * FROM users WHERE username='admin' --' AND password=''SELECT * FROM users WHERE username='admin' --' AND password=''Everything after -- is treated as a SQL comment the password check is completely stripped out. The query now only checks whether a user named admin exists, and logs the attacker in without ever knowing the real password.
A related classic payload, ' OR '1'='1, makes the WHERE clause always evaluate true, potentially returning every row in the table.
Key takeaway:_ It's not about_ filtering out dangerous characters it's about the database never treating user input as code in the first place.
The Correct Code (Parameterized Query)
app.post('/login', (req, res) => {
const { username, password } = req.body;
// SAFE: placeholders (?) - input passed as data, never as SQL code
const query = `SELECT * FROM users WHERE username = ? AND password = ?`;
db.query(query, [username, password], (err, results) => {
if (results.length > 0) res.send("Login successful");
else res.send("Invalid credentials");
});
});app.post('/login', (req, res) => {
const { username, password } = req.body;
// SAFE: placeholders (?) - input passed as data, never as SQL code
const query = `SELECT * FROM users WHERE username = ? AND password = ?`;
db.query(query, [username, password], (err, results) => {
if (results.length > 0) res.send("Login successful");
else res.send("Invalid credentials");
});
});With parameterized queries, the ? placeholders are sent to the database separately from the query structure. The driver treats admin' -- as a literal string to search for not as SQL syntax making injection structurally impossible, regardless of what characters the attacker types.
Prevention
- Parameterized queries / prepared statements the primary fix
- Server-side input validation (format, type, length) defense in depth, not the primary fix
- Never rely on client-side JS validation alone
- Principle of least privilege for database accounts
- Add MFA as a second layer, so a bypassed password check alone isn't sufficient
Practice It
- PortSwigger Lab: SQL injection - login bypass
- Practice site: testfire.net
- Practice site: http://testaspnet.vulnweb.com/login.aspx
- CyLab Security Academy: More SQL
- CyLab Security Academy: SQLiLite
- CyLab Security Academy: SQL Direct
- CyLab Security Academy: No Sql Injection
- CyLab Security Academy: Sql Map 1
4. Bypassing Multi-Factor Authentication (MFA)
MFA ≠ Automatically Secure
MFA provides an additional layer of protection beyond password-based login, typically combining two or more factors: something you know (password/PIN), something you have (a hardware token or OTP), and something you are (biometrics). But MFA is only as strong as its implementation and implementation flaws are extremely common.
Common Flaws
Hardcoded OTP / OTP reusability
Some apps ship with a static or reusable OTP value that never actually changes per session, making the "second factor" meaningless.
Missing brute-force protection on OTP validation
A 4 or 6-digit OTP has a small keyspace. Without rate limiting on the verification endpoint, tools like Burp Intruder can simply try every possible code:
Payload set: Numbers, 0000–9999, sequential
Request count: 10,000Payload set: Numbers, 0000–9999, sequential
Request count: 10,000
OTP visible in intercepted traffic
The OTP value is exposed somewhere in the request/response cycle that an attacker with proxy access (Burp Suite) can simply read.
HTTP response body/status code manipulation
Some client-side flows trust the server's response to decide whether the OTP was valid, rather than the server enforcing this decision authoritatively:
Original response:
403 Forbidden
{"error": true, "message": "Invalid Token"}403 Forbidden
{"error": true, "message": "Invalid Token"}Modified response (same wrong OTP, status changed):
200 OK
{"success": true}200 OK
{"success": true}Simply intercepting and rewriting the response status code convinces a poorly built client-side flow that the OTP was valid no correct code ever entered.
OTP/verification status stored in a client-trusted cookie or session token for example, a decoded Flask session cookie might reveal:
If the server trusts this client-readable value instead of independently verifying OTP status server-side, an attacker can simply edit the cookie.
Prevention
- OTP verification must happen fully server-side never trust a client-side "verified: true/false" flag
- Enforce rate limiting on OTP attempts short numeric OTPs are brute-forceable fast without this
- Short OTP expiry window
- Never expose the OTP in intercepted traffic/responses, or store it in a client-readable cookie
- Bind the 2FA step to the same session/context as the original login attempt
Practice It
- PortSwigger Lab: 2FA simple bypass
- CyLab Security Academy: No FA
- CyLab Security Academy: IntroToBurp
5. Session Hijacking
What It Is
Session hijacking is an attack in which a bad actor gains unauthorized access to a user's active website or application session exploiting weaknesses in session management to impersonate a legitimate user entirely.
How It Works
Once you log in, the server issues a session identifier (usually a cookie) that represents "you" for every subsequent request. If that identifier is stolen through network sniffing, cross-site scripting, or simply being copied from an unprotected session, the attacker can paste it into their own browser and instantly become that user. No password, no MFA, no SQL injection needed. Just copy, paste, and you're in.
This is what makes session hijacking such an important closing point: even a system with strong passwords, solid rate limiting, and properly implemented MFA can still be completely undone by weak session handling.
Prevention
- Set HttpOnly and Secure flags on session cookies (prevents JavaScript access, forces HTTPS-only transmission)
- Rotate session IDs on login and on privilege changes
- Short session expiry + server-side session invalidation on logout
- Bind sessions to additional context (IP, User-Agent, device fingerprint) where feasible
- Use the SameSite cookie attribute to reduce cross-site exposure
Practice It
- Practice site: testfire.net
- PortSwigger Lab (XSS-based cookie theft variant): Exploiting XSS to steal cookies
6. JWT - Token-Based Authentication Bypass
What a JWT Is
JSON Web Tokens (JWT) are used to authenticate users instead of traditional session cookies. After a user authenticates with valid credentials, the server issues a signed token used to authorize subsequent requests. A JWT has three parts, separated by dots:
HEADER.PAYLOAD.SIGNATUREHEADER.PAYLOAD.SIGNATURE- Header - token type and signing algorithm
- Payload - claims about the user (e.g., role, user ID)
- Signature - proves the token wasn't tampered with
Critically: the header and payload are only base64-encoded, not encrypted. Anyone can decode and read them. Only the signature created using a private key or secret is supposed to make the token trustworthy.
JWTs break in a few distinct ways depending on what the server gets wrong. Here are the three most common.
Vulnerability 1 - The alg:none Bypass
Some server implementations blindly trust whatever algorithm the token's own header claims. A normal token might look like:
{"alg": "HS256", "typ": "JWT"}
{"sub": "1234567890", "name": "John Doe", "role": "user"}{"alg": "HS256", "typ": "JWT"}
{"sub": "1234567890", "name": "John Doe", "role": "user"}If an attacker changes the header's algorithm to "none" and strips the signature entirely:
{"alg": "none", "typ": "JWT"}
{"sub": "1234567890", "name": "John Doe", "role": "admin"}{"alg": "none", "typ": "JWT"}
{"sub": "1234567890", "name": "John Doe", "role": "admin"}A vulnerable server may skip signature verification altogether, since it was told "there is nothing to check" and the role change from user to admin goes through unverified. This is known as the "none algorithm" vulnerability, one of the most well-documented JWT flaws.
The authentication flaw here is really just the entry point, the actual damage is broken access control. Once the forged token is accepted, the attacker has gained admin rights they were never authorized for, which is exactly the vertical privilege escalation problem covered in the companion post. Authentication bypass and access control failures aren't always separate incidents; often, breaking one is just the technique used to reach the other.
Vulnerability 2 - Weak / Crackable Signing Secret
A JWT's signature is only as strong as the secret used to generate it. Many implementations sign tokens with HS256 using a short, guessable, or dictionary-word secret (e.g., "secret", "password123").
Because the signature is just an HMAC of the header + payload using that secret, an attacker can take a valid, legitimately-issued token, and run it offline against a wordlist (using a tool like hashcat or john with a JWT-cracking mode) to recover the secret:
hashcat -a 0 -m 16500 token.txt rockyou.txthashcat -a 0 -m 16500 token.txt rockyou.txtOnce the secret is recovered, the attacker can re-sign their own forged token with "role": "admin" in the payload and the server will accept it as fully valid, since the signature now genuinely matches. Unlike the alg:none bypass, this attack produces a properly signed token; the flaw isn't in skipping verification, it's in how weak the thing being verified against actually is.
Vulnerability 3 - Algorithm Confusion (RS256 → HS256)
Some applications use asymmetric signing (RS256): the server signs tokens with a private key and verifies them with a matching public key. The public key is, by design, not secret it's often published or embedded in client-side code.
The flaw: some JWT libraries don't strictly enforce which algorithm family should be used for verification. If a library is told to verify using HS256 (a symmetric algorithm) but the "secret" it's given happens to be the server's own public key, an attacker can:
- Obtain the server's public RSA key (often exposed via a
/jwks.jsonendpoint or similar) - Forge a new token, setting the header to
"alg": "HS256" - Sign it using HMAC, with the public key itself as the HMAC "secret"
Because the server ends up checking the signature using that same public key value, the forged HS256 signature validates even though the server intended to only ever verify RS256 tokens. This is called algorithm confusion, and it's especially dangerous because it doesn't require guessing anything; the public key is meant to be public.
Prevention
- The server must enforce its own expected algorithm server-side never trust the
algfield declared in the token - Use strong, unpredictable signing secrets (avoid weak/dictionary-crackable keys)
- Prefer asymmetric signing (RS256) over shared-secret HMAC where possible
- Set short token expiry and implement token revocation/blacklisting for logout or compromise scenarios
- Never store sensitive data in the payload it's readable, not encrypted
Practice It
- CyLab Security Academy: "JAuth" (
alg:nonebypass) - CyLab Security Academy: "JaWT Scratchpad" (weak/crackable signing secret a related but distinct JWT flaw)
- CyLab Security Academy: Java Code Analysis!?!
- PortSwigger: JWT attacks - learning path
- PortSwigger Lab: JWT authentication bypass via weak signing key
- PortSwigger Lab: JWT authentication bypass via algorithm confusion
The Common Thread
Across brute forcing, rate-limit bypass, SQL injection, MFA flaws, session hijacking, and JWT tampering one principle holds every single time:
Never trust the client. Verify everything identity, ownership, and permissions server-side, on every single request.
Further Reading & References
- OWASP Top 10 - Official Project Page
- OWASP A07:2021 - Identification and Authentication Failures
- PortSwigger Web Security Academy - Authentication
- PortSwigger Web Security Academy - JWT Attacks
- picoCTF / CyLab Security Academy - Practice Challenges
This post is part of a two-part series. Read the companion post on Broken Access Control covering IDOR, GUID/parameter tampering, and data leakage.
If you found this useful, follow me here on Medium and connect with me on LinkedIn I'd love to hear what you build or break using these labs.