System designCore10 min
Common attack classes
Most web attacks are the same mistake in different places: data from an attacker is treated as code, or a request is trusted because of where it seems to come from. Injection, cross-site scripting, request forgery and server-side request forgery each have a known, boring defense; the breaches happen where it was skipped, or in a dependency nobody patched.
1 · The pattern
Most attacks turn data into code, or borrow someone else's authority
A community recipe site lets people sign up, post recipes, comment, search, and import a recipe by pasting a link to another site. Every one of those features takes input from strangers. The classic attacks are the ways that input can be misread:
Attack | What gets confused | Recipe-site example |
|---|---|---|
SQL or command injection | Data is run as a query or command | A search term that ends the query and starts another |
Cross-site scripting (XSS) | Data is run as script in other users' browsers | A comment containing a script |
Cross-site request forgery (CSRF) | Another site's request carries your login | A hidden form that deletes your recipes |
Server-side request forgery (SSRF) | Your server fetches what an attacker chooses | An import link pointing at an internal address |
Unsafe deserialization | Data rebuilds objects that run code | A crafted saved-session blob |
The OWASP Top 10 for 2025 ranks the broader categories: broken access control first, then security misconfiguration, software supply chain failures, cryptographic failures, and injection fifth. Access control has its own page (Authentication & authorization); this one covers the classes above and the dependencies they hide in. Each has a defense that works in a design, not just a filter that guesses.
2 · Injection
Injection is fixed by keeping data and code apart, not by escaping
The recipe search builds its SQL by gluing the user's text into a string. A search for cake' OR '1'='1 changes the query's meaning; a longer one can read other tables. The first defense is to never glue at all, but to send the query and the data separately, as a prepared statement with parameters:
Search: vulnerable, then safe
# Vulnerable: the input becomes part of the SQL text
cur.execute("SELECT id, title FROM recipes WHERE title LIKE '%" + q + "%'")
# Safe: the query is fixed; the input is only ever a value
cur.execute("SELECT id, title FROM recipes WHERE title LIKE %s", (f"%{q}%",))With parameters, the database receives the query's structure before it sees any user data, so no input can change what the query does. The defenses run from prepared statements, through carefully built stored procedures and allow-list validation, to escaping all user input, the weakest of them: escaping is easy to get wrong in one place out of hundreds. Parts of a query that can't be parameters, such as a sort column, should be chosen from a fixed list.
Command injection is the same mistake with the operating system's shell. The recipe site converts recipes to PDF by running a command with the recipe title in it; a title containing ; rm -rf becomes a second command. The main defense is to avoid calling OS commands directly and use a library function instead. Where a command is unavoidable, pass arguments as a list rather than a shell string, validate them against an allow list, and run the process with the least privilege it needs.
3 · XSS
Cross-site scripting runs an attacker's script in other users' browsers
A comment on a popular recipe contains a script tag that sends the reader's session data to the attacker. If the site renders comments as raw HTML, every reader runs it:
Stored XSS: the script is saved once and served to everyone who opens the page, running with each reader's session and permissions on the site.
No single technique solves XSS; it takes a combination:
Defense | What it does |
|---|---|
Encode output for its context | Text in HTML is entity-encoded, so a script tag is shown, not run; attributes, JavaScript and URLs each need their own encoding |
Let the framework do it | Modern frameworks auto-escape by default; the risk moves to escape hatches that insert raw HTML |
Sanitize allowed HTML | If comments may contain formatting, keep only safe tags with an HTML sanitizer |
Content Security Policy | Restricts what scripts a page may load or run; easy to get wrong, so an extra layer, not the main defense |
HttpOnly session cookies | Scripts can't read them; limits theft, but doesn't stop the script acting as the user |
4 · CSRF
Request forgery rides on the cookies a browser sends automatically
Browsers attach a site's cookies to requests going to that site, even when another site triggered the request. A reader logged in to the recipe site visits a different page that contains a hidden form posting to the recipe site's "delete all my recipes" endpoint. Without protection, the browser sends the session cookie along, and the recipe site sees a valid, logged-in request:
With a CSRF token or a SameSite cookie, the forged request either arrives without the secret token or without the session cookie, and is refused.
Defense | How it stops the forged request |
|---|---|
SameSite=Strict cookie | The cookie is sent only on requests that start from the same site |
SameSite=Lax cookie | Also sent on top-level cross-site navigations with safe methods such as GET, not on cross-site POSTs; some browsers apply Lax by default |
Anti-CSRF token | A secret, server-generated token in each form is checked on submit; another site can't read it |
No state changes on GET | Links and image tags can't trigger changes |
Origin check | State-changing requests from other origins are refused, as an extra layer |
One warning ties this section to the last: cross-site scripting can defeat every CSRF defense, because a script running on the site itself can read the token.
5 · SSRF
Server-side request forgery makes your server fetch what the attacker chooses
The "import recipe from a link" feature fetches whatever URL the user pastes. An attacker pastes an internal address instead, and the recipe site's server, sitting inside the network, fetches it on the attacker's behalf. In cloud environments SSRF is often used to steal credentials from the instance metadata service:
The import service can reach places the attacker can't. Whatever it fetches can come back to the attacker in the imported 'recipe'.
The defenses depend on whether the destinations can be known in advance. If they can, use an allow list of permitted hosts. If any public URL must be accepted, as with recipe imports, four rules apply.
- Resolve the name and check the resulting address, refusing private, loopback and link-local ranges, and connect to the address you checked, since an attacker can make a name resolve differently the second time.
- Turn off redirects in the fetching client, or check each hop, because a public URL can redirect to an internal one.
- Fetch from an isolated network segment or through an egress proxy that can't reach internal services (Webhooks, which have the same problem).
- Harden the metadata service: on AWS, require IMDSv2 and disable IMDSv1.
6 · Dependencies
The same holes arrive through libraries and data formats
Much of an application's code is other people's, and attacks follow it there. Two incidents show the shape, from their official records:
Incident | What happened | Lesson |
|---|---|---|
Log4Shell, 2021 (CISA) | Logging an attacker's string made the Log4j library fetch and run code from the attacker's server | Find every vulnerable instance, patch at once starting with internet-facing systems, review logs |
Equifax, 2017 (US GAO) | An unpatched Apache Struts flaw in a dispute portal; personal data of at least 145.5 million people accessed | Patch, keep detection working (an expired certificate hid the traffic), segment database access |
Both are injection and patching problems at once, which is why the OWASP Top 10 now ranks software supply chain failures third. The defenses are inventory and speed: know every library and version in every service, so a new vulnerability can be matched to affected systems in hours; patch on a schedule and urgently when needed; and limit what any one compromised component can reach (Network segmentation & zero trust).
Deserialization is the data-format version of the same risk. Many languages have native formats that rebuild arbitrary objects from bytes, with features that can be repurposed. Attacks on deserializers of untrusted data have led to denial of service, access-control bypass and remote code execution. Accept untrusted data only in plain data formats such as JSON, validate it against a schema, and never feed native serialized objects from users, cookies or other services into a deserializer.
7 · In practice
Use the design defense first, and layers after it
Attack | Primary defense | Extra layers |
|---|---|---|
SQL injection | Parameterized queries | Allow lists for identifiers, least-privilege database users |
Command injection | Library calls instead of shells | Argument lists, allow lists, low privileges |
XSS | Contextual output encoding, framework auto-escaping | Sanitizer for rich text, CSP, HttpOnly cookies |
CSRF | Tokens and SameSite cookies | No state-changing GETs, Origin checks |
SSRF | Allow list, or checked addresses without redirects | Isolated egress, hardened metadata service |
Vulnerable dependencies | Inventory and fast patching | Segmentation, monitoring |
For the recipe site, the checklist:
- Never build queries or commands from strings; parameters and libraries only.
- Encode everything you render, and know your framework's escape hatches.
- Protect every state change with tokens and SameSite cookies.
- Treat every server-side fetch of a user URL as hostile.
- Keep an inventory of dependencies and a way to patch all of them within days.
- Add a WAF and monitoring as layers, not as the fix (DDoS protection & WAFs).
Check yourself
5 questions
Takeaways
Remember this
- Most attack classes treat attacker data as code, or trust a request because of where it seems to come from.
- Prevent injection with parameterized queries and library calls instead of string-built queries and shells.
- Prevent XSS with contextual output encoding and framework auto-escaping, with CSP and HttpOnly as extra layers.
- Prevent CSRF with tokens and SameSite cookies, and SSRF with allow lists or checked addresses and isolated egress.
- Many breaches come through dependencies: keep an inventory, patch fast, and never deserialize untrusted native objects.