Skip to content

10 β€” Web Application Attacks

Level: Intermediate Β· Time: ~20 min Β· Prerequisites: Lesson 9 β€” Network Attacks


Why this matters

Your website and your line-of-business application are the only systems you are obliged to expose. They must accept input from strangers, they must hold or reach the data worth stealing, and they are written by people under deadline who were told to make it work. That combination makes the application the most-attacked surface you own, and the reason so many breaches begin with a flaw in code nobody reviewed rather than a clever network trick. The good news is that the flaws repeat. There are about ten categories, they have been stable for years, and each has a known fix. This lesson teaches the categories, the fix for each, and the small set of habits that removes most of the risk.

[!IMPORTANT] Attack techniques in this lesson are described for recognition and defence only. A vulnerable code snippet is paired with its fixed version so you can recognise the shape in your own code β€” not to rehearse against anybody else's.


The mental model: three inputs, one boundary

Every web attack is one of three things going wrong at the boundary between untrusted input and trusted behaviour:

What goes wrong The question you ask Example category
Input is trusted as data when it is code is this input ever parsed as instructions, a query, a path or markup? injection, XSS, deserialisation
Action is trusted as allowed when it is not checked does the server verify this user may do this thing, to this object? broken access control, auth failures
Trust is placed in the environment is this platform, library, key or pipeline what I think it is? misconfiguration, vulnerable components, integrity failures

Forget the names for a moment and remember the questions. Most real-world findings are answered by "we never asked that".


The OWASP Top 10 (2021)

The OWASP Top 10 is a periodically updated consensus list of the most serious categories of web application risk, derived from real data plus practitioner survey. It is not a checklist of bugs and it is not a standard you can be certified against β€” it is a shared vocabulary and a starting point.

Category In one sentence One concrete example The fix
A01 Broken Access Control the application does not check whether this user may do this thing change ?invoice=1042 to 1043 and read someone else's invoice β€” an IDOR server-side authorisation on every request, deny by default, never rely on the UI hiding a link
A02 Cryptographic Failures data is unprotected in transit or at rest, or protected with the wrong primitive passwords stored as unsalted SHA-256; card data posted over plain HTTP TLS everywhere with HSTS, slow salted password hashing (bcrypt, scrypt, Argon2), encryption at rest, managed keys
A03 Injection untrusted input is interpreted as instructions by a parser a login form whose input is concatenated into a SQL statement parameterised queries, allowlisting, never building shell commands from input
A04 Insecure Design the flaw is in the design, so no amount of clean code fixes it a password-reset flow that reveals whether an address exists, or a discount code with no rate limit to brute-force threat modelling, abuse-case testing, limits on sensitive flows
A05 Security Misconfiguration the platform is left at its default or most permissive setting default management-console credentials; directory listing on; verbose stack traces shown to users hardening baselines, remove defaults, disable unused features, automated configuration checks
A06 Vulnerable and Outdated Components you are running someone else's known-broken code a CMS plugin with a published flaw that was patched upstream a year ago dependency inventory, patch discipline, software composition analysis, delete unused components
A07 Identification and Authentication Failures the login itself is weak no lockout, so credential stuffing succeeds; the session ID is not regenerated at login, so a pre-set one becomes authenticated MFA, throttling and lockout, session regeneration on login, breach-password screening
A08 Software and Data Integrity Failures code or data is trusted without verifying where it came from a native deserialiser rebuilding attacker-controlled objects; a build pipeline consuming an unsigned artefact verify signatures and integrity, avoid native deserialisation of untrusted data, harden the pipeline
A09 Security Logging and Monitoring Failures the breach happened and nobody noticed no log of failed logins or authorisation denials, no alert when an account gains a new role log authentication and authorisation decisions, centralise, alert, and test that alerts fire (Lessons 22 and 44)
A10 Server-Side Request Forgery the server makes a request the attacker chose, on the attacker's behalf a "fetch this image from a URL" feature used to reach the cloud instance metadata endpoint and read credentials allowlist destinations, block link-local and metadata addresses, egress control, never return the raw response

Read the fix column downwards: it is mostly the same four ideas β€” validate, authorise, encrypt, keep components current.


Access control and injection, in detail

Two categories cause the majority of serious findings, so they deserve a worked example each.

Broken access control

The UI hides other people's records; the endpoint does not check. This is why "the button is not visible" is not a control.

# Vulnerable: any authenticated user can read any invoice by guessing an id
@app.get("/invoice/<int:invoice_id>")
def invoice(invoice_id):
    inv = Invoice.query.get(invoice_id)
    return inv.to_dict()
# Fixed: the query is scoped to the caller, so an unauthorised id returns 404
@app.get("/invoice/<int:invoice_id>")
def invoice(invoice_id):
    inv = Invoice.query.filter_by(id=invoice_id, owner_id=current_user.id).first_or_404()
    return inv.to_dict()

The habits that follow: authorise on the object, not just the route; check function-level rights server-side rather than trusting that the admin page is only linked for admins; and log every denial, because a run of denied attempts is a reliable intrusion signal (Lesson 44).

Injection

Injection happens when a string built from user input is handed to something that parses it as instructions β€” SQL, a shell, an LDAP filter, an XML parser.

# Vulnerable: the input is concatenated, so it becomes part of the SQL
cursor.execute("SELECT * FROM users WHERE name = '" + user + "'")
# Fixed: the statement shape is fixed and the value is bound as data
cursor.execute("SELECT * FROM users WHERE name = ?", (user,))

Parameterised queries are not a hardening measure you can skip when in a hurry; they remove the vulnerability class entirely, because the database is told "this is a value" rather than left to work it out. The same principle covers command injection (do not build shell strings; pass arguments as a list, or use a library that does it for you) and LDAP injection (escape and validate before building a filter). Input validation is still worthwhile as defence in depth, but it is never the primary control, because allowlists you write by hand are exactly the sort of thing that misses a case.


The cross-category classics

These overlap the Top 10 categories but appear so often in real findings that they are worth naming individually.

Attack What it is The control
Reflected XSS the payload arrives in the request and is echoed back into the page, so a crafted link executes script in the victim's browser context-aware output encoding, CSP as defence in depth
Stored XSS the payload is saved β€” a comment, a profile field β€” and served to everyone who views it the same encoding, plus sanitising rich-text input with a vetted library
DOM-based XSS client-side JavaScript writes untrusted data into the page without the server seeing the payload at all avoid dangerous sinks, encode before writing to the DOM, CSP
CSRF the browser automatically attaches cookies to requests, so a page elsewhere can trigger a state-changing action the user never intended anti-CSRF tokens plus SameSite=Lax or Strict cookies; re-authenticate on sensitive actions
XXE an XML parser resolves external entities, letting a document read local files or make network requests disable DTD and external-entity processing; use a simpler format where XML is not required
File upload to web shell an uploaded file lands in a directory the web server executes, and is then requested store uploads outside the web root, validate by content rather than extension, never execute an upload directory
Path traversal ../ sequences in a filename parameter escape the intended directory canonicalise the path and confirm it is inside the base directory; prefer opaque identifiers to paths
Open redirect a ?next= parameter lets a phishing link carry your domain accept only relative paths from an allowlist
Clickjacking the site is framed invisibly and a click is captured for another purpose frame-ancestors in CSP, with X-Frame-Options only for legacy clients

Two general points bind them together. First, encoding must match the context: text in HTML, a value in a JavaScript string, a value in a URL and a value in CSS are four different escaping problems, which is why frameworks that escape automatically are worth using and not fighting. Second, Content-Security-Policy does not fix XSS β€” the bug is still there β€” but it reliably stops an injected script from executing, which is exactly what defence in depth means.


Why APIs shift the emphasis

Modern applications do their real work in APIs, and APIs fail differently.

API-specific issue Why it happens The control
Broken object level authorisation (BOLA) the web UI checks ownership before calling the API, so the API itself never learned to; the dominant real-world API flaw enforce object authorisation in the API, not the client β€” the same scoping shown earlier
Mass assignment binding request fields directly onto a model lets a caller set fields the UI never offered, such as is_admin allowlist bindable fields explicitly; never bind a whole object from a request
Excessive data exposure the endpoint returns the whole record and lets the client decide what to show return only the fields the caller is entitled to see
No rate limiting APIs are built for machines, so nobody noticed the login endpoint answering ten thousand times a minute throttle by identity and by source, and alert on the pattern
Schema and version sprawl an old API version is still live after the current one added its authorisation checks inventory versions, retire old ones, require authentication on every version

CSRF does not translate directly to token-authenticated APIs β€” the risk shifts to stolen tokens, which is why token lifetime, scope and revocation matter. What does translate is the first row: authorisation must live on the server, per object, in every interface you expose.


The defender's practical list

If you build, or commission, or review web applications, these eight items remove most of the risk and each one is testable.

  1. Parameterise every query. No string-built SQL, ever, in any code path.
  2. Encode output for its context, preferably via a framework that does it by default and is not overridden.
  3. Deny by default on authorisation, and check every object against the caller on every request.
  4. Validate and canonicalise on the server. The client is a display layer; anything it enforces can be removed with a proxy.
  5. Keep a dependency inventory and patch on a schedule you can prove, not when a news story forces you.
  6. Set the security headers: HSTS, a Content-Security-Policy that suits the application, X-Content-Type-Options: nosniff, a Referrer-Policy, and frame-ancestors to stop framing.
  7. Log authentication and authorisation decisions, including denials, and alert on patterns rather than on single events.
  8. Rate-limit anything sensitive β€” login, password reset, voucher or discount redemption, search, and any endpoint that costs money per call.

Run these against your own application deliberately, in a test environment. Lesson 27 covers testing web applications with a defined scope, and Lesson 43 covers what to do when you run one in production.


Attack it / Defend it

The attack How it works The control that stops it
IDOR / broken access control an identifier in the request is changed to reach another user's record per-object authorisation on the server, deny by default, 404 rather than 403 for unauthorised objects
SQL injection input is concatenated into a statement and parsed as SQL parameterised queries, least-privilege database accounts, input validation
Command injection input is interpolated into a shell command never build shell strings from input; pass argument lists or use libraries
Stored XSS a saved payload executes for every visitor of the page context-aware output encoding, CSP, sanitised rich text
CSRF the browser's automatic cookie attachment is abused for a state change anti-CSRF tokens, SameSite cookies, re-authentication on sensitive actions
XXE an XML document references an external entity the parser resolves disable DTD and external entities; prefer JSON
File upload to web shell an uploaded file is stored where the server executes it store outside the web root, validate content, never execute uploads
SSRF to cloud metadata a URL-fetching feature is pointed at the instance metadata endpoint destination allowlists, block link-local addresses, egress control
Credential stuffing against the login leaked username and password pairs are replayed at scale MFA, throttling and lockout, breach-password screening
Mass assignment a request field the UI never offers is bound onto the model explicit allowlists of bindable fields

Key takeaways

  • The web application is the surface you cannot hide. It is exposed by design, so its flaws are reachable by anyone with a browser.
  • Three questions catch most flaws: is this input ever treated as code; did the server check that this user may do this; and is this component or platform what I think it is.
  • Authorisation belongs on the server, per object, in every interface. A hidden button has never stopped an attacker.
  • Parameterised queries and context-aware output encoding remove entire vulnerability classes rather than reducing their likelihood.
  • Logging failures decide how bad a breach becomes. An unlogged authorisation denial is a missed detection, not a tidy log.

Check yourself

  1. A colleague says "we hide the admin page from normal users, so that is covered". What is the flaw in that reasoning, and what would you check?
  2. Why is parameterisation better than escaping user input for SQL, even when the escaping looks correct?
  3. Give one attack where the payload never reaches the server, and explain why output encoding still matters.
  4. An API returns the full customer record and the app displays a subset. Which API risk is present, and what is the fix?
  5. Which single logging change would most improve your ability to detect a broken-access-control attack in progress?

Next

Lesson 11 β€” Malware and Ransomware