Broken authentication and session management: proving who you are, and staying proven
Authentication is the gate every other control stands behind: if an attacker can become the victim, authorization checks are answering the wrong question. It sits in OWASP Top 10:2025 A07 — Authentication Failures, tested per WSTG-ATHN. You meet it in bounty programs (login, reset, and session endpoints are always in scope and always attacked), in SOC work (authentication logs are the highest-volume telemetry you will ever triage), and in AppSec reviews (session handling is where homegrown auth goes wrong).
Level 3 · Apply — do it unaided on an unfamiliar authorized target, and explain why it worked and what would have stopped it
Walkthrough run 2026-10-09 · local demo login; MFA enrollment step not run by us
| Stage | Stage B · Practitioner |
|---|---|
| Level | Level 3 · Apply — do it unaided on an unfamiliar authorized target, and explain why it worked and what would have stopped it |
| Prerequisites | a-http-depth, b-csrf, b-idor (full list in §2) |
1. Where this shows up
Authentication is the gate every other control stands behind: if an attacker can become the victim, authorization checks are answering the wrong question. It sits in OWASP Top 10:2025 A07 — Authentication Failures, tested per WSTG-ATHN. You meet it in bounty programs (login, reset, and session endpoints are always in scope and always attacked), in SOC work (authentication logs are the highest-volume telemetry you will ever triage), and in AppSec reviews (session handling is where homegrown auth goes wrong).
2. Before you start
- You can read Set-Cookie headers and cookie attributes cold (
a-http-depth). - You understand the authentication/authorization split from
b-idor: this lesson is the who are you half. - Authorization and scope, stated plainly: the walkthrough attacks a login app on your own loopback interface with accounts you created. Credential stuffing, password spraying, and brute force are taught here as concepts with defenses — you do not run them against any real system, any account that isn't yours, or any target outside written scope. "It was only a few login attempts" is not a defense; rate-limiting exists precisely because those attempts are attacks.
- Tooling: Python 3 (stdlib only), curl.
3. How it works
Authentication answers "who are you"; the session is how the server remembers the answer between requests. HTTP is stateless, so after a successful login the server hands the client a session token (usually a cookie) and keeps the mapping server-side: token → identity. Every later request presents the token, and the server trusts it. That trust is the entire attack surface of this lesson. There are four ways it breaks:
1. The token is guessable (token hygiene). A session id of sess-1001, sess-1002 is a counter, not a secret — the attacker doesn't steal a session, they enumerate it. Session tokens must be generated by a cryptographic random source, long enough to be unguessable (128 bits is the working minimum), and created fresh at login. The same rule covers password-reset tokens: unguessable, single-use, expiring. A reset link that stays valid forever is a second password that travels by email.
2. The token doesn't change at login (session fixation). If the server accepts a session id the client chose — or keeps the pre-login session id after authentication — the attacker plants a known session id in the victim's browser (a crafted link setting the cookie, a subdomain writing it), waits for the victim to log in, and then uses the same id. The victim authenticated the attacker's session. The fix is one line of discipline with a memorable shape: the session id must be rotated at every privilege change — login, logout, privilege escalation. A post-login session id must never equal the pre-login one.
3. The gate has no brakes (missing rate limiting). Without rate limiting, lockout, or anomaly detection on login and reset endpoints, the attacker gets unlimited guesses. Two concepts, kept distinct: brute force guesses passwords for one account (defeated by lockout + slow, expensive verification); credential stuffing replays username/password pairs leaked from other breaches (defeated by rate limiting + breach-password screening, because the passwords are correct — the account is just reused). (Breach-password screening — checking each candidate password against lists of passwords known from breaches, and rejecting matches — is the control NIST SP 800-63B requires of verifiers.) The defense is never "complexity requirements" alone; it is making each guess expensive and few.
4. One factor isn't enough (MFA as the structural fix). A password is "something you know," and everything knowable is phishable, leakable, and stuffable. Multi-factor authentication adds a second, independent factor — "something you have" (a TOTP authenticator app, a hardware key) or "something you are." (TOTP — Time-based One-Time Password, RFC 6238: a 6-digit code derived from a shared secret and the current time, changing every 30 seconds.) Why a possession factor is structurally different from a knowledge factor: a password is information, and information leaks — it can be phished, breached, and replayed from anywhere. A hardware key or authenticator app is a thing; the attacker must compromise the device itself, at the moment of use. Stuffing attacks replay leaked information at scale; there is no leaked-information equivalent for the thing in your pocket. MFA doesn't fix broken session handling, but it structurally defeats credential stuffing: the stuffed password alone is no longer sufficient. Note the honest limit, which the walkthrough states plainly: MFA protects authentication, not the session — a stolen post-login session cookie bypasses MFA entirely, which is why token hygiene and cookie flags still matter.
Worked example: the demo login
The demo app has /login (username + password, accounts alice/wonderland-42 and bob/builder-99 — local only, created for this lesson). On success it sets sid and serves /dashboard. Three flaws, each visible in one response header or one behavior:
Set-Cookie: sid=s1001— sequential. Log in as alice, notes1001; log in as bob, notes1002. The next session id is predictable: request/dashboardwithsid=s1003and watch what happens.- Log in with a planted cookie (
Cookie: sid=ATTACKER-KNOWNsent to/login): after successful login the response sets no newsid— the session keeps the planted id. Fixation works. - Twenty rapid login attempts with wrong passwords: all return
401, none returns429, no lockout, no delay. The gate has no brakes.
The fixed build: sid is 32 hex chars from a cryptographic RNG, rotated on every login (the response always sets a new one), and /login rate-limits to 5 attempts per minute per IP with 429 beyond that. Each fix is verified by re-running the exact probe that demonstrated the flaw.
4. Walkthrough
Run by us on this machine — transcript: runs/b-session-auth.log. Clean environment: stock Python 3.12, curl 8.5.0, loopback only, 2026-10-09. The demo (auth-demo.py, stdlib only) runs the login app above on 127.0.0.1:18095. Both accounts are local, created for this lesson, and exist nowhere else.
Step 1 — start the demo. Run python3 auth-demo.py 18095 &; binds 127.0.0.1:18095.
Step 2 — baseline: log in as alice. curl -s -c jar -X POST -d "user=alice&pass=wonderland-42" http://127.0.0.1:18095/login. Response: ok, and the jar holds sid=s1001. curl -s -b jar http://127.0.0.1:18095/dashboard → dashboard for alice. The session mechanism works as designed.
Step 3 — token predictability. Log in as bob (-c jar2): sid=s1002. Sequential. Now, with a fresh jar, request the dashboard with the predicted next id: curl -s -b <(echo '127.0.0.1 FALSE / FALSE 0 sid s1003') http://127.0.0.1:18095/dashboard — in the transcript this is done with an explicit Cookie: sid=s1003 header. If any session had been created in between, s1003 would be live. The point stands regardless of the hit: the id space is enumerable, so the attacker doesn't need to steal anything.
Step 4 — session fixation. Send a login request carrying a planted session id: curl -s -c jar3 -b "sid=PLANTED-BY-ATTACKER" -X POST -d "user=alice&pass=wonderland-42" http://127.0.0.1:18095/login. Inspect jar3: the sid is still PLANTED-BY-ATTACKER — the server kept the attacker-chosen id across the privilege change. Anyone holding that id now shares alice's session. This is the fixation, proven in one response.
Step 5 — no brakes. Twenty wrong-password attempts in a loop: for i in $(seq 1 20); do curl -s -o /dev/null -w "%{http_code} " -X POST -d "user=alice&pass=wrong-$i" http://127.0.0.1:18095/login; done. All twenty return 401. No 429, no lockout, no growing delay. Count the codes in the transcript: twenty 401s, zero consequences.
Step 6 — the fix, all three. Restart with FIXED=1. Log in as alice: sid is now 32 hex characters (e.g. 9f2c…), different on every login — rotation verified by logging in twice and comparing. Fixation re-test: planted sid=PLANTED-BY-ATTACKER → after login the jar holds a fresh random id; the planted value is dead. Rate-limit re-test: the sixth rapid attempt returns 429; the response includes Retry-After. The login also sets HttpOnly; SameSite=Lax on the cookie (the flags lesson is b-security-headers; here they are noted as present).
Step 7 — MFA, the structural fix (concept + enrollment shape). The demo does not implement real MFA — a toy second factor would teach the wrong lesson. Instead: the fixed app's /login response includes mfa-required: true for alice, documenting the enrollment contract the lesson teaches: the server stores a per-user TOTP secret (never the code), verification is a time-windowed check of a 6-digit code from the user's authenticator app, and recovery codes are single-use. Not run by us: enrolling a real authenticator app — the reader does this against their own accounts (GitHub, their password manager) as the lesson's "on your own" task. The honest limit is stated on the page: MFA defeats credential stuffing; it does not protect a stolen session cookie.
Step 8 — defender: read the auth log. The vulnerable run's log shows the three abuses in their natural telemetry: the fixation as a sid value that appears before any login and persists after one; the brake-less run as twenty 401s from one IP in seconds; the predictable ids as a visible sequence. Concretely:
01:12:03 POST /login 401 user=alice ip=203.0.113.44
01:12:03 POST /login 401 user=alice ip=203.0.113.44
... twenty 401s, one IP, four seconds — the gate with no brakes
01:12:09 POST /login 200 user=alice sid=PLANTED-BY-ATTACKER <- the sid predates the login
That last line is the fixation visible in one row: the session id was presented before authentication and survived it. Detection rules write themselves: alert on >N failed logins per IP per minute, on session ids presented before authentication, on sid values with low entropy. (Entropy — the amount of unpredictability in the value; low entropy means few guesses needed.) This is the highest-volume log source in any SOC — which is why the rules must be tuned, not just written.
Step 9 — stop the demo. pkill -f auth-demo.py; verify no demo processes remain.
5. On your own
Pick one account you own (your password manager, your GitHub, your email): enroll a TOTP authenticator app, store the recovery codes offline, and then answer from the service's own documentation — not from memory: does it rate-limit login attempts, does it notify on new sign-ins, and what does its password-reset flow expire? Success test: you can describe the service's actual controls, with the documentation pages cited, and name one control it lacks that this lesson taught.
6. Where it goes wrong
- Confusing authentication with authorization. This lesson is who are you;
b-idoris what may you do. A perfect login flow with no object-level checks is still fully vulnerable — the two halves are independent and both are required. - "The password policy is strong, so we're fine." Complexity rules don't stop credential stuffing (the passwords are correct) and don't slow brute force (that's rate limiting's job). Teach the threat each control addresses, or you'll deploy the wrong one.
- Session id in the URL. Tokens in query strings leak via history, logs, proxy logs, and Referer headers. Cookies with
Secure; HttpOnly; SameSiteexist for this reason — a token that travels in the URL is a token published. - Rotation theater. Regenerating the session id but keeping the old one valid, or rotating only sometimes, preserves the fixation window. The test is absolute: post-login id ≠ pre-login id, and the old id is dead.
- MFA as a talisman. MFA on login with session cookies that never expire, tokens without HttpOnly, and no rotation is a locked front door with the windows open. The session is the authentication after login — protect it like one.
- Lockout as a DoS vector. Aggressive lockout ("5 failures locks the account") lets anyone deny service to any user by trying their username. Prefer progressive delays + alerting over hard lockout, or lock the source, not the account.
7. Read the source
Primary documents — read in this order:
- PortSwigger Web Security Academy — Authentication vulnerabilities (portswigger.net/web-security/authentication): the flaw families — brute force, MFA bypass logic, password-reset poisoning, session handling. Read the topic page fully; work the labs (free). Verified in-session 2026-10-09.
- OWASP Authentication Cheat Sheet (cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html — verified 2026-10-09): the control catalog — password storage, lockout, MFA, reset flows. Use as the checklist your implementations are graded against.
- RFC 6265 §4 — HTTP State Management Mechanism (rfc-editor.org/rfc/rfc6265): what a cookie is — attributes,
Set-Cookiesemantics, the security considerations section. Read §4.1 and the security section; skim the rest. Verified in the pilot (sources.csv).
Free resources for going deeper (≤3): PortSwigger authentication labs (brute-force, 2FA bypass, reset-token labs — the hands-on companion); the b-security-headers lesson (cookie flags as deployed defense); your own services' security docs (the "on your own" task — primary sources for how production auth actually behaves).
8. Check yourself
- A server accepts
sidvalues chosen by the client and keeps them across login. Explain the fixation attack end to end: what the attacker plants, when, and what they gain — then state the one-line fix. - Why does credential stuffing defeat even a perfect password-complexity policy? Name the two controls that actually address it and what each one does.
- A login endpoint returns
429after 5 rapid failures from one IP. An attacker spreads attempts across 1,000 IPs, one attempt each. What broke, and what detection or control addresses the distributed version? - MFA is enabled and the attacker still hijacks the account without touching the password or the second factor. Describe a mechanism by which this is possible, referencing this lesson's session material.
- A password-reset token never expires and stays valid after use. An attacker recovers a year-old reset email from a breached mailbox. Explain what they can do with it that they couldn't if the token expired after an hour — then explain what changes if the token is also single-use, and which of the two properties matters more against an attacker who intercepts the email in transit.
9. Prove it
Artifact: a session-security assessment of a lab target (the local demo or a PortSwigger auth lab): token entropy judgment (with the evidence — sequential? length? charset?), the fixation test result (pre-login id vs post-login id), the rate-limit test result (attempt count vs first 429), cookie flags observed, and the fix verification for each — written as the authentication section of a real assessment report.
After this lesson you can honestly claim: you can assess a login and session implementation for the four breakages in this lesson, prove each locally, and specify the fix. You cannot yet claim: your assessments cover federation (OAuth/OIDC flows are a second-wave lesson) or that you can review MFA implementations for bypass logic — PortSwigger's 2FA-bypass labs are the next step, done by hand.
10. Leads to
Next: b-security-headers (cookie flags and browser defenses as the deployed layer for everything this lesson proved). Map refs: OWASP Top 10:2025 A07 — Authentication Failures; WSTG-ATHN; CWE-287 (improper authentication), CWE-384 (session fixation); feeds Stage C disclosure (auth findings are the most common first CVE) and the SOC-analyst path (auth logs are the triage bread and butter).
This lesson is one of one hundred ten in the security guidebook. Part of Stage B · Practitioner. See all one hundred ten lessons →