Almost everything is a web app or an API now, and every one is a bundle of inputs, sessions, objects and trust boundaries waiting to be abused. The attacks are rarely exotic — they're a handful of flaw classes, repeated endlessly. This page covers how web apps and APIs actually work, the OWASP Top 10 for both, each major vulnerability with worked examples and the fix, a testing methodology, and the controls that make an app secure by design.
Every interaction is a request from a client (browser, mobile app, script) to a server, which returns a response. HTTP is stateless — each request stands alone — so apps bolt state on top using cookies, sessions and tokens. The security of the whole thing lives in how the server trusts and handles what the client sends.
| Part | What it carries | Why it matters to an attacker |
|---|---|---|
| Method | GET, POST, PUT, DELETE, PATCH | Verb tampering can bypass weak access checks |
| URL & query string | Path + ?id=5&sort=asc | Parameters are the primary injection point |
| Headers | Cookie, Authorization, Host, User-Agent | Auth tokens live here; Host/headers can be forged |
| Body | Form data, JSON, XML, uploads | Where most injected payloads and mass-assignment live |
Browsers enforce the same-origin policy: script on site-a.com can't freely read responses from site-b.com. CORS headers relax that deliberately — and a misconfigured CORS policy (Access-Control-Allow-Origin: * with credentials) hands an attacker's page the ability to read authenticated responses. It's a common, quiet API flaw.
Most APIs are REST — resources at URLs (/api/users/5), actions via HTTP verbs. GraphQL exposes a single endpoint where the client specifies exactly what it wants. GraphQL's flexibility is also its risk: deeply nested queries can exhaust the server, and introspection often leaks the entire schema.
The OWASP Top 10 is the reference list of the most critical web application risks, rebuilt every few years from real-world data. The 2021 edition is the current stable release — know it cold:
| ID | Category | In one line |
|---|---|---|
| A01 | Broken Access Control | Users can act outside their permissions (IDOR, forced browsing) |
| A02 | Cryptographic Failures | Weak/missing encryption of data in transit or at rest |
| A03 | Injection | Untrusted input interpreted as code (SQLi, command, XSS) |
| A04 | Insecure Design | Flaws baked into the architecture, not just the code |
| A05 | Security Misconfiguration | Defaults, verbose errors, open cloud buckets |
| A06 | Vulnerable & Outdated Components | Known-CVE libraries and dependencies |
| A07 | Identification & Authentication Failures | Weak login, session and credential handling |
| A08 | Software & Data Integrity Failures | Unverified updates, insecure deserialization, supply chain |
| A09 | Security Logging & Monitoring Failures | Attacks go unseen and unanswered |
| A10 | Server-Side Request Forgery (SSRF) | The server is tricked into making attacker-chosen requests |
OWASP released a 2025 draft (being finalised through 2026). The headline changes: Broken Access Control stays at A01; "Vulnerable & Outdated Components" expands into Software Supply Chain Failures; and a new Mishandling of Exceptional Conditions category appears. The 2021 list above remains the stable reference until 2025 is fully adopted — learn 2021 first, then layer the changes on top.
Injection happens whenever untrusted input is mixed into something that gets interpreted — a database query, a shell command, an HTML page. The root cause is always the same: data and code aren't separated.
If a query is built by string concatenation, input becomes query logic:
# VULNERABLE — input is concatenated into the query
query = "SELECT * FROM users WHERE name = '" + name + "'"
# input: name = ' OR '1'='1 → returns every row
# SAFE — parameterised query; the driver keeps data ≠ code
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
Parameterised queries (prepared statements) are the definitive fix for SQLi — the database treats the input as a value, never as syntax, no matter what it contains. Input validation and WAFs help, but parameterisation is the control that actually closes it. The same principle applies to command injection: use safe APIs that take argument arrays, never a shell string.
XSS is injection into the browser: attacker-controlled input is reflected into a page and runs as JavaScript in the victim's session — stealing cookies, keylogging, or acting as them. Three flavours:
Payload bounces straight back from the request (e.g. a search term in the results page). Delivered via a crafted link.
Payload is saved server-side (a comment, profile field) and runs for every viewer. The most dangerous.
Client-side JS writes untrusted input into the page unsafely — never touches the server.
Fix: context-aware output encoding (HTML-encode on output), a strong Content-Security-Policy header, and framework auto-escaping. Never build HTML by concatenating user input.
The textbook access-control flaw, and the #1 risk overall. The app trusts an ID in the request without checking whether you are allowed that object:
GET /api/invoices/1043 → your invoice ✓
GET /api/invoices/1044 → someone else's ← no ownership check = IDOR
The fix is server-side authorisation on every object access — "does the current user own / have rights to this specific record?" — checked on the back end, every time. Never rely on the client hiding the option.
SSRF tricks the server into making a request the attacker chooses. Because that request originates from inside the network, it can reach things the attacker never could directly — internal services, admin panels, and most dangerously cloud metadata endpoints:
# app fetches a user-supplied URL for a "preview" feature
POST /api/fetch { "url": "http://169.254.169.254/latest/meta-data/" }
# → returns the cloud instance's IAM credentials. Full compromise.
SSRF was central to the 2019 Capital One breach — an SSRF flaw reached AWS metadata, stole credentials, and exfiltrated 100M+ records. Fix: allow-list permitted destinations, block requests to internal/link-local ranges, and require IMDSv2 on cloud instances.
Two distinct problems, often confused: authentication (are you who you claim?) and authorisation (are you allowed this?). Most breaches exploit a gap in one or the other.
HttpOnly (no JS access, blunts XSS theft), Secure (HTTPS only) and SameSite (blunts CSRF).SameSite cookies.JWTs are self-contained, signed tokens widely used for API auth — and widely misimplemented:
| JWT weakness | What goes wrong |
|---|---|
alg: none | Server accepts an unsigned token — forge any claims |
| Algorithm confusion | RS256 verified as HS256 using the public key as the secret |
| Weak signing secret | HS256 with a guessable key — crack it offline, sign your own |
| No expiry / no revocation | A stolen token works forever |
Defend with MFA, strong password policy + rate limiting (against credential stuffing), proper token expiry/rotation, and pinning the signing algorithm server-side. jwt_tool is the standard tester.
APIs expose logic and data directly, usually without a browser's protections, and often with thin or missing authorisation. OWASP maintains a separate Top 10 for them because the failure modes differ from classic web apps — authorisation dominates the list.
| ID | Risk | What it means |
|---|---|---|
| API1 | Broken Object Level Auth (BOLA) | IDOR for APIs — the #1 API risk. Accessing objects you don't own |
| API2 | Broken Authentication | Weak or missing token/credential validation |
| API3 | Broken Object Property Level Auth | Exposing or letting you edit fields you shouldn't (incl. mass assignment) |
| API4 | Unrestricted Resource Consumption | No rate/size limits — DoS and cost-explosion |
| API5 | Broken Function Level Auth (BFLA) | Calling admin functions as a normal user |
| API6 | Unrestricted Access to Business Flows | Automating a flow (e.g. bulk-buying) beyond intent |
| API7 | Server-Side Request Forgery | As above — common where APIs fetch URLs |
| API8 | Security Misconfiguration | Verbose errors, missing hardening, open CORS |
| API9 | Improper Inventory Management | Forgotten / undocumented / old API versions still live |
| API10 | Unsafe Consumption of APIs | Trusting third-party API data without validation |
If an API binds the whole request body to a model, adding a field the UI never shows can escalate you:
PUT /api/users/me { "name": "stew", "role": "admin" }
# the app blindly sets role → privilege escalation
Fix: explicitly allow-list which fields a request may set; never bind user input straight to a data model.
nuclei, subdomain enumeration.ffuf for fuzzing, sqlmap for SQLi.Test only what you're authorised to, within the agreed scope. In the UK, unauthorised testing breaches the Computer Misuse Act 1990. Practise legally on PortSwigger's Web Security Academy, OWASP Juice Shop, or your own lab.
| Control | Closes |
|---|---|
| Input validation + parameterised queries | Injection (SQLi, command, NoSQL) |
| Context-aware output encoding + CSP | Cross-site scripting (all types) |
| Server-side authz on every object & function | Broken access control, IDOR, BOLA, BFLA |
| HTTPS everywhere + security headers (HSTS, CSP, X-Frame-Options) | Transport & browser-layer attacks |
| Secure cookies (HttpOnly, Secure, SameSite) + anti-CSRF tokens | Session theft & CSRF |
| Rate limiting + WAF | Brute force, resource consumption, automated abuse |
| Field allow-listing | Mass assignment / property-level auth |
| Dependency scanning (SCA) + patching | Vulnerable & outdated components |
| Logging, monitoring & alerting | Undetected attacks (A09) |
NEVER TRUST INPUT · AUTHORISE EVERY REQUEST · ENCODE EVERY OUTPUT