Common Threats
Attach each common attack to the layer that stops it — bound parameters, output encoding, CSRF tokens, edge scrubbing — and size the traffic each absorbs.
Four attacks cover most of what a design interview asks about, and each has exactly one layer that is supposed to stop it. You don't need to be a security specialist to design against them, but you do need to name which layer catches which attack — a defense placed one tier too deep has already lost, because an injection that reaches the database as text is an injection that runs.
SQL injection
- What it is: An attack where malicious SQL code is inserted into user input fields. If the application builds its queries by concatenating strings, that code is executed by the database, letting an attacker bypass authentication, steal data, modify data, or gain administrative control.
- Mitigation:
- Prepared statements with bound parameters: the primary defense. It separates query logic from data. The database is told the query structure first, and user input is supplied afterwards as a parameter, so it is treated only as data and never as executable code.
- Use ORMs: modern ORMs use prepared statements underneath, which covers the ordinary query paths by default.
- Input validation: treat all user input as untrusted.
'; DROP TABLE users; -- is the folk version. The profitable version is quieter: ' OR 1=1 -- appended to a lookup turns a query that returned one row into one that returns the table. Against a 40M-row user table that is one well-formed request, invisible to every layer that only counts requests.
Bound parameters differ from escaping in mechanism, not in thoroughness. The query text reaches the parser once, on its own, and the parameter bytes arrive afterwards in a separate protocol field, so there is no moment at which they could become syntax — parsing already finished. Escaping is the rejected alternative: it makes safety depend on getting every encoding and quoting context right forever, does nothing in a numeric context where no quote is needed to break out, and mangles legitimate input, since O'Brien is a name rather than an attack.
Two gaps survive parameterisation. Identifiers cannot be bound — ORDER BY <column> <direction> is query structure, so a sort feature must map API-facing names onto an allowlist of real columns and reject the rest. And every ORM offers a raw-query escape hatch for the one report that was awkward to express; that line is where injection lives in an otherwise clean codebase. Bound the blast radius as well: the application's database role should hold no DDL and no access to tables outside its own.
Cross-site scripting
- What it is: An attack where malicious scripts (usually JavaScript) are injected into a page that other users then view. Their browser executes the script, which can steal session cookies, impersonate the user, or deface the site.
- Mitigation:
- Output encoding: before rendering user-generated content, encode it so the browser reads it as literal text rather than code (convert
<to<,>to>). Modern templating engines do this by default. - Content Security Policy: the
Content-Security-Policyheader tells the browser which sources of scripts, styles and images are trusted, so anything else does not execute.
- Output encoding: before rendering user-generated content, encode it so the browser reads it as literal text rather than code (convert
Three shapes, and one layer does not catch all of them. Stored XSS persists in your database and runs for every viewer; reflected XSS rides in a URL the victim is tricked into opening; DOM-based XSS never reaches the server at all, because client JavaScript writes untrusted text into innerHTML. Server-side templating covers the first two, so a single-page app can be perfectly encoded on the server and still be exploitable in the browser.
Encode on output rather than on input: the correct encoding depends on where the value lands — HTML body, attribute, URL parameter, inline script — and the handler that accepted the input cannot know which.
What the script gets is the victim's origin. HttpOnly stops it reading the session cookie but not issuing requests as the user with that cookie attached, which is also why XSS defeats CSRF tokens — a script on the page can read the token out of the form. Session handling is covered in authentication and authorization. CSP is the second layer, and one directive decides whether it does anything: a policy that keeps 'unsafe-inline' for scripts permits the exact injection it was added to prevent. Use per-response nonces or hashes, or you have a header rather than a policy.
Cross-site request forgery
- What it is: An attack that tricks an authenticated user's browser into making an unwanted request. A user logs into their bank, then visits a malicious page whose script triggers
POST /transfer?to=attacker&amount=1000from their browser. The browser attaches the session cookie automatically, so the bank sees a legitimate request. - Mitigation:
- Anti-CSRF tokens: the server issues an unpredictable per-session token, embeds it in forms, and validates it on submission. The attacker's page cannot read it, so it cannot forge a valid request.
- SameSite cookies:
SameSite=StrictorLaxtells the browser not to send the cookie on cross-site requests, removing most of the surface.
Browsers now default to Lax when the attribute is absent, which kills the classic cross-site form post. Two holes remain, and an interviewer goes straight to them. Lax still sends the cookie on top-level GET navigations, so anything that changes state on a GET is reachable from a plain link — "GET must not mutate" is a security rule, not only REST etiquette. And same-site is judged on the registrable domain, so promo.example.com, running a marketing tool nobody on the team controls, sits inside the boundary with api.example.com.
The trade-off worth naming is the credential itself. CSRF is a cookie problem: browsers attach cookies automatically and never attach an Authorization header on their own, so an API authenticated by a bearer token is not exposed at all. That removes the attack rather than mitigating it, at the price of keeping a token where JavaScript can reach it, which makes any XSS more valuable. Pick one and say which risk you took.
Denial of service, and the arithmetic that sizes it
- What it is: An attempt to make a resource unavailable by flooding it with traffic. A distributed attack drives it from many compromised hosts at once, so blocking a single source address accomplishes nothing.
- Mitigation: layered.
- Edge protection: a CDN or a dedicated scrubbing service absorbs and filters the flood before it reaches your servers.
- Rate limiting: apply rate limiting at the API gateway or load balancer, so one caller cannot exceed its share.
- Scalable infrastructure: scale horizontally, so ordinary spikes are absorbed rather than survived.
Numbers say why volumetric traffic has to die at the edge. A service taking 1M requests/day averages 1,000,000 ÷ 86,400 ≈ 12 QPS, call it 36 QPS at 3x peak, so the 100,000 QPS in the diagram is roughly 3,000x peak. At 200 QPS per app server, absorbing it needs 500 servers where two were running, and instances boot in tens of seconds against an attack that saturates in one. Autoscaling into it converts an outage into an outage with a larger bill; the capacity has to be pre-provisioned by someone whose business is having far more bandwidth than you.
The application-layer version needs no botnet, and it is the one that shows up. Take an unauthenticated search endpoint costing 200 ms and 2 queries per call, hit at 2,000 QPS from a single rented host. Little's Law gives 2,000 × 0.2 s = 400 requests in flight against an app pool of 100, so 300 queue and unrelated users time out, while 2,000 × 2 = 4,000 query QPS closes on the ~5,000 QPS a commodity Postgres box serves. The scrubbing edge sees nothing wrong — well-formed HTTP at a volume any CDN forwards. What stops it is a per-key limit at the gateway plus removing the cost asymmetry: cap the page size, require a cursor, and put the expensive path behind auth so there is a key to count.
In an interview
What is being tested is whether security appears as a layer in your design or as a list of words at the end of it. For each threat the interviewer wants three things: the mechanism, where the check runs, and what it costs.
Deliver them in one sweep while drawing the edge and the data layer, not in a closing paragraph. "Every query goes through prepared statements, so user bytes never reach the parser, and the app's database role has no DDL. User content is encoded at render, under a CSP with per-response nonces rather than 'unsafe-inline'. Session cookies are HttpOnly, Secure, SameSite=Lax, with a CSRF token on state-changing calls and nothing that mutates on GET. Volumetric floods are absorbed at the CDN; application-layer abuse is rate limited per API key at the gateway, and search sits behind auth because it costs 200 ms a call."
The mistake that loses points is naming the attack without naming the layer that stops it, or naming a defense that sits one tier too deep — answering SQL injection with "we sanitise input" instead of "the parser never sees user bytes" says you have the word and not the mechanism. The runner-up is offering autoscaling as DDoS protection, which answers a capacity question with a billing answer. Third is claiming SameSite closed CSRF without mentioning mutating GETs or your own subdomains.
Check yourself
1. An unauthenticated /search endpoint costs 200 ms and 2 database queries per call. One rented host sustains 2,000 requests/s against it. Which component fails first, and does a DDoS scrubbing service help?
Little's Law:
2,000 × 0.2 s = 400requests in flight. An app pool of 100 holds 100 and queues the rest, so the application tier saturates first and unrelated requests behind it time out. The database is close but not yet over —2,000 × 2 = 4,000query QPS against the ~5,000 a commodity box serves. Scrubbing does not help: this is well-formed HTTP at a volume the edge forwards. The fixes are a per-key rate limit at the gateway and a cheaper endpoint — cap the page size, require a cursor, put it behind auth so there is a key to limit.
2. A reporting screen lets the user choose the sort column and direction. Your ORM parameterises every value. Is the feature safe as built?
No. Placeholders substitute values, and
ORDER BY <column> <direction>is structure, so no driver offers a placeholder for it — the only way to build that clause is concatenation, the vulnerable pattern with an ORM around it. The decision is an allowlist: a fixed map from API-facing names to real columns plusASC/DESC, and a 400 for anything unmatched. Escaping the identifier is the rejected alternative, because it re-opens the encoding question bound parameters were adopted to close, for a field with about six legal values.
3. Your SPA on app.example.com calls api.example.com, the session lives in a cookie with SameSite=Lax, and a teammate proposes deleting the CSRF tokens. Take a side.
Keep them, or change the credential.
Laxblocks the cross-site POST but still sends the cookie on top-levelGETnavigations, so any state change reachable byGETstays exploitable; and same-site is judged on the registrable domain, so every*.example.comhost — including one running a third-party tool — is inside the boundary and can send credentialed requests. Either keep per-session tokens on state-changing calls and forbid mutatingGETs, or move the credential into anAuthorizationheader the browser never attaches by itself, which removes CSRF outright and accepts a token JavaScript can read — strictly worse under XSS, which is why output encoding and CSP stay non-negotiable either way.