Learn to hack for real.

Free, beginner-friendly walkthroughs of PortSwigger Web Security Academy labs — written by Michael Dahan, ranked #38 in the world on the PortSwigger Hall of Fame (all 274 labs solved). Every solution ships with a runnable script — see them all on GitHub.

SQL InjectionCross-Site Scripting (XSS)AuthenticationAccess Control

SQL Injection

PortSwiggerApprentice
SQL injection vulnerability in WHERE clause allowing retrieval of hidden data
SQL injection has been on the OWASP Top 10 for over two decades, and it survives not because developers don't know it exists, but because of exactly...
PortSwiggerApprentice
SQL injection vulnerability allowing login bypass
A login form is usually the single most attacked endpoint on any application, and it's also where SQL injection stops being a data-disclosure bug and...

Cross-Site Scripting (XSS)

PortSwiggerApprentice
Reflected XSS into HTML context with nothing encoded
Cross-site scripting is the vulnerability class that makes "don't trust the client" a rule instead of a suggestion — every reflected XSS bug ultimately...
PortSwiggerApprentice
Stored XSS into HTML context with nothing encoded
Stored XSS is the more dangerous sibling of reflected XSS for a simple reason: the payload doesn't need a victim to click a crafted link, it just needs...
PortSwiggerApprentice
DOM XSS in document.write sink using source location.search
DOM-based XSS breaks the mental model the first two labs built up: there is no server-side reflection to find, because the vulnerable data flow happens...
PortSwiggerApprentice
DOM XSS in innerHTML sink using source location.search
This lab looks almost identical to the previous one on the surface — the same location.search source, the same search box, the same absence of any...
PortSwiggerApprentice
DOM XSS in jQuery anchor href attribute sink using location.search source
Every DOM XSS lab so far has involved raw HTML injection — breaking out of an attribute or supplying an event handler that the browser parses as...
PortSwiggerApprentice
DOM XSS in jQuery selector sink using a hashchange event
Every DOM sink so far has fired on page load, triggered by a query parameter we could put straight in a URL and send to a victim as a link. This lab...
PortSwiggerApprentice
Reflected XSS into attribute with angle brackets HTML-encoded
The first reflected lab in this series had no encoding at all, so a plain <script> tag was enough. This lab adds the first real defense we've seen:...
PortSwiggerApprentice
Stored XSS into anchor href attribute with double quotes HTML-encoded
This lab combines two things we'd only seen separately until now: a stored injection point (the comment form's "website" field, rather than a one-shot...
PortSwiggerApprentice
Reflected XSS into a JavaScript string with angle brackets HTML encoded
This lab moves the injection point somewhere new: inside a JavaScript string literal embedded directly in the page, rather than in HTML markup or an...

Authentication

PortSwiggerApprentice
Username enumeration via different responses
A login form only needs to say one thing to an attacker: "wrong." The moment it says two different kinds of wrong — one for a username that doesn't...
PortSwiggerApprentice
2FA simple bypass
Two-factor authentication only works if the server treats "password verified" and "fully authenticated" as different states. If a session is marked...
PortSwiggerApprentice
Password reset broken logic
A password reset token is only a security control if it's actually checked at the moment it matters — when the new password gets saved, not just when...

Access Control

PortSwiggerApprentice
Lab: Unprotected admin functionality
Broken access control has topped the OWASP Top 10 for years, and the simplest version of it is also the easiest to miss: a page that does exactly what...
PortSwiggerApprentice
Lab: Unprotected admin functionality with unpredictable URL
Hiding a sensitive endpoint behind a random-looking URL feels like it should work — there's no robots.txt entry to leak it and no wordlist likely to...
PortSwiggerApprentice
Lab: User role controlled by request parameter
Some applications decide who you are once, at login, and then trust whatever the client hands back on every request after that. When that trust is...
PortSwiggerApprentice
Lab: User role can be modified in user profile
Profile update endpoints tend to be treated as low-risk by developers — what harm could changing your own email address do? But if the same request...
PortSwiggerApprentice
Lab: User ID controlled by request parameter
Horizontal privilege escalation doesn't need a broken role system — it just needs an identifier that names which record to return, sitting in a place...
PortSwiggerApprentice
Lab: User ID controlled by request parameter, with unpredictable user IDs
Swapping a username in an id parameter is trivial when usernames are the identifier. Switch that identifier to a GUID and the naive version of the...
PortSwiggerApprentice
Lab: User ID controlled by request parameter with data leakage in redirect
Redirecting an unauthorized request away from sensitive data looks like an access control fix on the surface — the browser never renders the page it...
PortSwiggerApprentice
Lab: User ID controlled by request parameter with password disclosure
An IDOR that leaks another user's API key is bad. An IDOR that leaks the administrator's password turns a horizontal information leak into full...
PortSwiggerApprentice
Lab: Insecure direct object references
Not every IDOR lives in a URL query parameter pointing at a database row. Static files — transcripts, exports, generated documents — are direct object...

New writeups added regularly — bookmark this page.

Want to go from zero to junior pentester?

These walkthroughs are a taste. The full path — live, hands-on, small cohorts — starts with a free webinar.

Join the Free Live Webinar →