Skip to content

Core Web Vulnerabilities

This page walks the bug classes you will actually find on modern web and API targets, using a terminal-centric workflow built on curl and small scripts. For each class you get the same three things: the signal to look for, how to confirm it, and the impact. The theme throughout is detect and minimally prove — never destroy, never exfiltrate real user data, never DoS.

Almost every vulnerability on this page is one bug: the server trusted something it should have checked. It trusted that the user ID in your request was your own. It trusted that a URL you supplied was safe to fetch. It trusted that text you sent would never reach another user’s browser. When you test, you are constantly asking: what did the server take on faith, and what happens if I break that faith?

Keep a proxy running while you work so you can see and replay every request — see mitmproxy and nmap.

IDOR (Insecure Direct Object Reference), called BOLA (Broken Object-Level Authorization) in API terms, is the single most common and most valuable API bug. The server exposes an object by an identifier — /api/orders/1052 — and fails to check that this user is allowed to see object 1052.

The signal. Any request that references an object by an ID you can change: numeric IDs in the path or query, IDs in the JSON body, IDs in a header. Sequential integers are the classic tell, but UUIDs and hashes are just as vulnerable if the server never checks ownership.

How to confirm. Create two accounts you control, A and B. Do an action as A and capture the request. Replay it verbatim but with B’s session cookie/token, changing only the object ID to one that belongs to A.

Terminal window
# As user A, note an order id you own: 1052
# Now replay it using user B's session, asking for A's order:
curl -s https://target.example/api/orders/1052 \
-H "Authorization: Bearer $TOKEN_B" | jq .

If B receives A’s data, you have a horizontal IDOR. Confirm it is a real leak, not an empty stub, by comparing against the same call made as A. Two IDs and a diff is the whole proof — you never need to enumerate thousands of records.

Impact. Reading, modifying, or deleting other users’ data. Vertical IDOR (a normal user reaching admin-only objects) is privilege escalation. Cross-tenant IDOR — reaching another customer’s whole account — is usually the highest-severity bug on a multi-tenant platform.

SSRF is when you make the server send a request of your choosing. Because the server sits inside the trusted network, it can reach things you cannot: internal admin panels, databases, and cloud metadata endpoints.

The signal. Any feature where you hand the server a URL or hostname and it fetches it: webhook URLs, “import from URL”, link previews / unfurling, PDF/screenshot generators, image proxies, XML parsers, url= parameters.

Full vs blind SSRF. In full (in-band) SSRF the fetched response is shown back to you. In blind SSRF you get no response body — you only learn the request happened by observing it land somewhere you control.

How to confirm — safely. Point the server at a collaborator/canary host you own and watch for the callback. This proves the server made an outbound request without touching any internal system.

Terminal window
# Use a listener you control (e.g. a small VPS running `nc -lvnp 80`,
# or a request-catcher service you own). Give the app that URL:
curl -s https://target.example/api/link-preview \
-H "Content-Type: application/json" \
-d '{"url":"http://your-canary.example/ssrf-test-1"}'
# Then check your listener: did a request for /ssrf-test-1 arrive,
# and what User-Agent / source IP did it come from?

If your canary logs the hit, SSRF is confirmed. For a slightly stronger (still safe) proof, show the server can resolve an internal-only name — many programs accept a DNS lookup of something like an internal hostname as evidence without you ever reading sensitive data.

Impact. Access to internal services, cloud credentials, and lateral movement. Even blind SSRF is serious because it can hit unauthenticated internal endpoints. Filters that only block localhost or 127.0.0.1 are commonly bypassed with 127.1, [::1], 0.0.0.0, decimal/hex IPs, or a DNS name that resolves to an internal address — mention the bypass conceptually in your report rather than weaponizing it.

XSS is getting your JavaScript to run in another user’s browser in the target’s origin. That runs with the victim’s session, so it can read their data and act as them.

Three kinds:

Type Where the payload lives How it fires
Reflected In a request parameter, echoed straight back in the response Victim clicks a crafted link
Stored Saved server-side (comment, profile name, product title) Fires for anyone who views it
DOM-based Never touches the server; client-side JS writes attacker input into the page Fires from a URL fragment or client state

The signal. Your input appears verbatim in a response. Send a unique, harmless marker and grep for it.

Terminal window
# Send a unique marker and see if it comes back unescaped:
curl -s 'https://target.example/search?q=zzqmarker123' | grep -o '.\{20\}zzqmarker123.\{20\}'

Look at where it lands. zzqmarker123 inside <p>zzqmarker123</p> is reflected in HTML body context. Inside value="zzqmarker123" it’s an attribute. Inside <script>var q="zzqmarker123"</script> it’s a JS-string context. Context decides the payload and whether escaping is missing.

How to confirm — benignly. The community-standard harmless proof is:

<script>alert(document.domain)</script>

alert(document.domain) is ideal because it proves execution and prints the origin the script ran in — which is exactly what a triager needs to see. Prefer a screenshot of that alert over anything that touches cookies or makes network calls.

Why context matters: the same input is safe in one place and dangerous in another. Escaping for HTML (&lt;) does nothing inside a <script> block; escaping for JS does nothing inside an href. If you see your marker reflected but a naive payload doesn’t fire, the fix is usually to break out of the specific context (close the attribute/quote/tag), not to give up.

Impact. Full account takeover in the victim’s session, CSRF-token theft, defacement, and phishing on a trusted origin. Stored XSS on a shared page is worst — it hits every viewer.

CSRF tricks a logged-in victim’s browser into making a state-changing request to the target. It works because browsers attach cookies automatically to any request to a site, even one triggered from a different site.

When it applies. CSRF is only interesting for requests that (a) change state, (b) rely purely on an ambient credential like a cookie, and (c) have no unpredictable token. Endpoints authenticated by an Authorization: Bearer header that the browser does not auto-send are generally not CSRF-able.

SameSite changed the landscape. Modern browsers default cookies to SameSite=Lax, which stops cookies being sent on most cross-site sub-requests (like a hidden form POST from another origin). That neutralizes classic CSRF for many endpoints — but not all. Lax still allows top-level GET navigations, SameSite=None cookies are fully exposed, and a state-changing GET remains vulnerable. Always check the actual cookie flags:

Terminal window
curl -s -i https://target.example/login -d 'user=me&pass=...' | grep -i '^set-cookie'
# Look for: Secure; HttpOnly; SameSite=Lax|Strict|None

How to confirm — the PoC concept. Build a minimal HTML page that auto-submits the target request and load it while logged in as your own test account. If the action happens with no token, it’s CSRF. Keep the action trivial (change your own display name) so nothing destructive occurs.

<!-- csrf-poc.html — tests against YOUR OWN account only -->
<form action="https://target.example/account/email" method="POST">
<input type="hidden" name="email" value="csrf-poc@your-mailbox.example">
</form>
<script>document.forms[0].submit()</script>

Impact. Any state change the victim can make: change email (then password-reset to it), change settings, add a shipping address, transfer funds. Impact is whatever the forgeable endpoint does.

An open redirect is an endpoint that sends the browser to a URL taken from user input without validating it: …/redirect?next=https://evil.example.

The signal. Parameters named next, url, return, redirect, dest, continue, callback.

How to confirm. Ask for a redirect to a harmless external domain you control and read the Location header — no need to actually follow it.

Terminal window
curl -s -i 'https://target.example/login?next=https://example.com/' | grep -i '^location'
# => Location: https://example.com/ ← open redirect confirmed

Impact. On its own, open redirect is usually low severity (phishing that borrows the target’s trusted domain). Its real value is as a primitive: it makes SSRF filter bypass easier, and it can turn a strict OAuth redirect_uri check into a token leak — see auth attacks. Report the chain, not just the redirect.

Injection is when input crosses out of data and into a command interpreter — SQL, a shell, an LDAP query, a template engine. The detection mindset is the same everywhere: send input that would be a syntax error if interpreted, and watch for the interpreter reacting.

SQL injection — responsible detection. The safest first probe is a single quote and a logically neutral payload, watching for behavior differences, not dumping data.

Terminal window
# 1) Error-based signal: a lone quote may break the query.
curl -s 'https://target.example/item?id=10%27' | grep -iE 'sql|syntax|odbc|sqlite|postgres|mysql'
# 2) Boolean signal (non-destructive): compare a true vs false condition.
curl -s "https://target.example/item?id=10" -o a.html
curl -s "https://target.example/item?id=10+AND+1=1" -o b.html # should match id=10
curl -s "https://target.example/item?id=10+AND+1=2" -o c.html # should differ
diff <(cat a.html) <(cat c.html) >/dev/null && echo "same (no signal)" || echo "content changed → possible SQLi"

If 1=1 returns the normal page and 1=2 returns a different/empty page, the input is being parsed as SQL. That difference is the proof — you do not need UNION SELECT dumps or ---comment chains to demonstrate the bug.

Command injection — the signs. Look for features that shell out: ping/traceroute tools, file converters, archive handlers, anything taking a filename. The signal is that a shell metacharacter changes behavior. The safe proof is a benign, observable side effect — a DNS lookup to your canary — rather than reading /etc/passwd or running destructive commands.

Terminal window
# Benign proof-of-concept: cause the server to resolve YOUR canary hostname.
curl -s 'https://target.example/tools/ping' \
-d 'host=8.8.8.8;nslookup ssrf-test.your-canary.example'
# Then check your DNS logs for the lookup. A callback = command execution.

Impact. SQLi ranges from authentication bypass to full database read; command injection is typically remote code execution on the server. Both are critical — which is exactly why your proof should be the smallest possible signal.

These are bugs where every individual request is “valid” but the workflow lets you do something you shouldn’t. No scanner finds them; you find them by understanding the business rules and asking “what if I break the intended order or values?”

Common shapes:

  • Price / value tampering. The client sends the price or discount and the server trusts it. Send a request with "price": 0.01 or a 100% discount and see if the total honors it.
  • Quantity edge cases. Negative quantities that credit you, quantities that overflow, adding an out-of-stock or restricted item by ID.
  • Workflow skipping. Reaching step 4 (confirm) without completing step 3 (payment). Replay the final request directly.
  • Coupon / gift-card reuse. Applying a single-use code twice, or stacking codes that shouldn’t stack.
Terminal window
# Value tampering probe — against YOUR OWN test cart/order only:
curl -s https://target.example/api/cart/apply-discount \
-H "Authorization: Bearer $MY_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"code":"WELCOME10","percentage":100}' | jq '.cart.total'
# If total drops to 0 because the server trusted your "percentage", that's a logic flaw.

Race conditions (the concept). Some logic checks and then acts in two steps — “is this coupon unused? then use it.” Fire many identical requests simultaneously and several may pass the check before any of them records the use (a TOCTOU, time-of-check-to-time-of-use, gap). Classic outcomes: redeeming a one-time coupon many times, or withdrawing the same balance twice.

Impact. Direct financial loss (free/underpriced orders, duplicated credit), inventory abuse, and bypassed limits. Business-logic bugs are prized precisely because they’re specific to the target and can’t be caught by generic defenses.

  • Every bug here is a broken trust assumption. Ask what the server took on faith.
  • Prove with the smallest signal: two IDs for IDOR, one callback for SSRF, alert(document.domain) for XSS, one boolean difference for SQLi.
  • Test with accounts and objects you own; never touch other users’ real data.
  • Escalation and destruction are for the report’s impact narrative, not for your live requests.
  • Chain low-severity primitives (open redirect, reflection) into higher-impact findings when you can — and report the chain.