---
title: "API Security in Depth: Rate Limiting, CORS, CSRF, Input Validation, and Security Headers"
description: "A practical guide to API security: rate limiting and 429, what CORS does and does not protect, CSRF defenses, input validation, and security headers."
canonical: "https://www.suriyaprakash.in/blog/engineering/api-security-rate-limiting-cors-csrf-validation"
category: "Engineering"
date: "2026-09-05"
updated: "2026-09-05"
series: "Production Backend Systems"
tags:
  - api-security
  - cors
  - csrf
  - rate-limiting
  - input-validation
  - security-headers
  - backend
---

# API Security in Depth: Rate Limiting, CORS, CSRF, Input Validation, and Security Headers

Canonical: https://www.suriyaprakash.in/blog/engineering/api-security-rate-limiting-cors-csrf-validation

Part 5 built a permission system: given an authenticated user, one place decides what they may do to which record. That decision only helps if the request reaches it in a sane form, and if the surrounding API does not give an attacker a way around it.

This part is about that surrounding layer: the controls that sit between the open internet and your handlers. They are less glamorous than authentication and authorization, and they account for a lot of real incidents, because they are easy to configure almost-correctly. A CORS header that looks right and protects nothing. A CSRF defense that covers `POST` but not the endpoint someone added last month. A rate limit keyed on a header the caller controls.

Throughout, I separate three things: what a standard says, what I tested myself, and what is a recommendation. The CORS section includes a two-origin experiment I ran in a real browser, because the most common misconception about CORS is easy to settle that way.

## Quick answer

- **Rate limiting protects availability and cost.** Limit by the identity you trust, return `429`, and put hard caps on body size, page size and expensive operations too.
- **CORS is a browser rule about reading responses. It is not access control.** A blocked cross-origin request can still have reached your server and run.
- **CSRF matters when the browser attaches credentials automatically** (cookies, mostly). Defend with tokens or request-origin checks, and treat `SameSite` as an extra layer, not the whole defense.
- **Validate input on the server, against an allow-list, at the edge.** Pass the validated output onward, never the raw body.
- **Security headers are cheap hardening.** For a JSON API the useful set is small.
- **Errors are part of the API surface.** Be consistent, be generic about internals, and use the right status code.

```txt
internet
   |
   v
[ TLS + headers ] -> [ rate limit ] -> [ CORS / CSRF ] -> [ parse + validate ]
                                                                  |
                                                                  v
                                          [ authenticate ] -> [ authorize (Part 5) ] -> handler
                                                                  |
                                                                  v
                                                 [ generic, consistent error responses ]
```

## Where this fits in the OWASP API Top 10

The OWASP API Security Top 10 (2023 edition) is a useful map of what goes wrong. Its list, in order, is: Broken Object Level Authorization, Broken Authentication, Broken Object Property Level Authorization, Unrestricted Resource Consumption, Broken Function Level Authorization, Unrestricted Access to Sensitive Business Flows, Server Side Request Forgery, Security Misconfiguration, Improper Inventory Management, and Unsafe Consumption of APIs.

You have already covered several of these in this series: authentication (Parts 2 to 4) and the three authorization entries (Part 5). This post addresses most of the rest:

| OWASP item | Covered here by |
|---|---|
| Unrestricted Resource Consumption | Rate limits, body and page-size caps, timeouts |
| Security Misconfiguration | CORS, headers, verbose errors |
| Unrestricted Access to Sensitive Business Flows | Per-flow limits (signup, reset, checkout) |
| Server Side Request Forgery | Allow-listing outbound destinations (short note below) |
| Improper Inventory Management | Knowing which endpoints and versions exist |

## Rate limiting and resource consumption

A rate limit answers "how much may one caller do in a period?" Without one, a single client can exhaust your database connections, your third-party API quota, or your bill. Several different attacks reduce to the same weakness: credential guessing (Part 4), scraping, enumeration, and plain denial of service.

### What to limit, and by what key

The key decides whether the limit protects you or can be sidestepped:

- **Per IP address** is the baseline for anonymous endpoints, but many users share one IP (offices, mobile carriers) and one attacker can use many.
- **Per authenticated user or API key** is more accurate once you know who is calling.
- **Per account and IP together** suits sensitive flows such as sign-in, as Part 4 showed: slow guessing at one account and slow one address sweeping many accounts.
- **Per sensitive flow** matters on its own. A limit on "password reset emails per account per hour" protects the mailbox and your sending reputation, regardless of general traffic.

One trap deserves a callout: **the client IP behind a proxy.** Proxies and CDNs pass the original address in a header such as `X-Forwarded-For`, and a caller can send that header themselves. Only trust forwarded-address headers that were set by a proxy you control, and take the entry that proxy appended, not the first value a client supplied. Otherwise an attacker changes one header per request and resets their own limit. This is a recommendation based on how the header works, so check how your platform exposes the true client address.

### Algorithms, and the boundary problem

| Algorithm | How it works | Tradeoff |
|---|---|---|
| **Fixed window** | Count requests per clock-aligned window (for example per minute) | Simple; a caller can send a full quota at the end of one window and another at the start of the next, so up to twice the limit within a short span |
| **Sliding window** | Count over the last N seconds, continuously | Smoother; more state or computation |
| **Token bucket** | A bucket refills at a steady rate; each request spends a token | Allows controlled bursts, easy to reason about |

The boundary effect in the fixed window row follows directly from how it counts. If the limit is 100 per minute, 100 requests at second 59 and 100 more at second 61 are both "within limit" in their own windows, yet 200 arrive in about two seconds. Pick sliding window or token bucket where that matters.

### What the standards say about the response

The status code is `429 Too Many Requests`, defined in RFC 6585. That RFC says the response **may** include a `Retry-After` header telling the client how long to wait, that such responses **must not** be stored by a cache, and that it does not define how you identify the user or count requests; that is up to you. The OWASP REST Security Cheat Sheet likewise says to return 429 when requests arrive too quickly.

A set of `RateLimit` and `RateLimit-Policy` response headers for advertising quotas is being worked on in the IETF HTTP API working group. At the time of writing it was still an active Internet-Draft (version 11, dated 23 May 2026, with no RFC number), so treat the field names as emerging rather than settled and do not depend on clients understanding them.

### Limits that are not about request counts

Rate limiting counts requests, but a single request can be enormously expensive. OWASP's API item is titled *Unrestricted Resource Consumption* for a reason. Put explicit caps on:

- **Request body size.** OWASP's REST cheat sheet says to reject requests over the limit with `413 Request Entity Too Large`.
- **Page size.** A `limit` parameter with no maximum lets one request ask for a million rows. Validate it with an upper bound (I show a schema below).
- **Query cost.** Expensive searches, exports and reports need their own, lower limits and often a background job instead of a synchronous response.
- **Timeouts.** A request that can run forever will eventually be used to hold your connections open.
- **Outbound calls.** If your API fetches a URL a caller supplies (a webhook target, an avatar by URL), allow-list the destinations. Otherwise the server becomes a way to reach internal addresses. That is the idea behind the SSRF entry in the Top 10.

Rate limiter state has to be shared if you run more than one instance: a counter held in one server's memory lets a caller multiply their quota by the number of servers. A shared store, or your platform's built-in rate limiting, avoids that.

## CORS: what it does, and what it does not

CORS (Cross-Origin Resource Sharing) is the mechanism by which a server tells browsers which other origins may read its responses. By default, a script on `https://a.example` cannot read the response of a request to `https://b.example`. CORS headers loosen that restriction in specific ways.

The most important sentence in this section: **CORS is enforced by the browser, on the response. It does not stop a request from being sent, and it does nothing at all for requests that do not come from a browser.** `curl`, a server-side script, and a mobile app ignore it entirely. MDN's description matches this: browsers use CORS to mitigate the risks of cross-origin requests made from scripts.

### The experiment

I wrote two tiny servers on different ports of `localhost` (so different origins), served a page from one, and had its script call the other with `fetch`. The API logged everything it received. The API sent no CORS headers except where noted.

| # | Request from the page | What the script could do | What the server actually received |
|---|---|---|---|
| 1 | `POST`, `Content-Type: text/plain`, body `transfer=100`, no CORS headers | **Blocked** (a `TypeError`) | The full `POST` with its body |
| 2 | `POST`, `Content-Type: application/json`, no CORS headers | **Blocked** | Only an `OPTIONS` preflight. The `POST` was never sent |
| 3 | The same JSON `POST`, server allows this origin and header | Response readable (200) | `OPTIONS`, then the `POST` |
| 4 | Credentialed `fetch`, response says `Allow-Origin: *` with credentials | **Blocked** | The `POST` |
| 5 | Credentialed `fetch`, explicit allowed origin but no `Allow-Credentials` | **Blocked** | The `POST`, with the cookie attached |

Rows 1, 4 and 5 are the lesson: **the browser blocked the script from reading the response, yet the server received the request and ran it.** If that request had been "transfer money", the money would have moved. The block is about who can *see* the result.

Row 2 shows why this does not apply to every request. A request with a JSON content type is not a "simple" request, so the browser sends a preflight `OPTIONS` first, and without permission the actual `POST` is never sent. MDN defines simple requests as those using `GET`, `HEAD` or `POST` with only safelisted headers, and a content type of `application/x-www-form-urlencoded`, `multipart/form-data` or `text/plain`. Anything else is preflighted.

A caveat on row 5: both of my origins were on `localhost`, which browsers treat as the same *site* even though the ports make them different *origins*. That is why a `SameSite=Lax` cookie was attached. For truly cross-site requests, whether the cookie is sent is governed by the cookie's `SameSite` setting, which is a separate layer from CORS (see the CSRF section).

### Two consequences for your API

**1. Do not treat CORS as a defense against forged writes.** A simple request, such as a form-style `POST`, reaches your server whether or not CORS is configured. If your handler accepts it, the side effect happens. The defense for that is CSRF protection, below.

**2. Reject unexpected content types.** The reason row 2's request never arrived is the JSON content type forcing a preflight. That protection disappears if your server parses a body as JSON regardless of the `Content-Type` header: an attacker's page can then send JSON text labeled `text/plain` as a simple request. OWASP's REST guidance is to validate the content type and reject unsupported ones with `415 Unsupported Media Type`. Make that a rule of your body parser, not an optional check.

### Configuring CORS correctly

When you do need cross-origin access, for example a frontend on one domain calling an API on another:

- **Allow-list exact origins.** Respond with the specific matching origin, not `*`, when credentials are involved.
- **Never reflect the `Origin` header back blindly.** Echoing whatever origin arrives is the same as allowing everyone, with credentials. Compare against an explicit list.
- **Add `Vary: Origin`** when the allowed origin depends on the request, as MDN advises, so caches do not serve one origin's response to another.
- **Credentials need explicit values.** For credentialed requests, MDN says `Access-Control-Allow-Origin`, `-Headers`, `-Methods` and `-Expose-Headers` must not use the `*` wildcard; you need explicit values. Row 4 shows the browser blocking a wildcard with credentials, and row 5 shows it blocking a response without `Access-Control-Allow-Credentials`.
- **Cache preflights.** `Access-Control-Max-Age` defaults to 5 seconds per MDN, and browsers cap the value, so a modest explicit setting saves a round trip without promising more than the browser will honor.

## CSRF: when the browser sends credentials for you

CSRF (Cross-Site Request Forgery) tricks a logged-in user's browser into sending a state-changing request to your site. It works because browsers attach cookies automatically: if the user is signed in to your app and visits a hostile page, that page can cause a request to your app that arrives with their session cookie.

The risk exists whenever authentication is something the browser attaches by itself. The cookie sessions from Part 2 qualify. A bearer token that your own JavaScript puts in an `Authorization` header does not have the problem, because a hostile page cannot read or attach it. That is a reason people give for token-in-header designs, but it comes with the storage tradeoffs Part 2 described, so it is not free.

### The defenses, as OWASP lays them out

The OWASP CSRF Prevention Cheat Sheet recommends, roughly in this order:

1. **Use your framework's built-in CSRF protection** if it has one.
2. **Tokens.** For stateful apps, the synchronizer token pattern (a per-session random token that must accompany each state-changing request). For stateless apps, a signed double-submit cookie, which OWASP marks as recommended (the unsigned form is marked discouraged).
3. **Custom request headers** for APIs that do not use HTML forms. A custom header forces a preflight, as row 2 showed, so a cross-origin page cannot send it without your permission.
4. **Fetch Metadata**, for software that targets only modern browsers: the `Sec-Fetch-Site` header tells you whether a request came from the same origin, and OWASP calls it the most useful of those headers. It requires a fallback to origin verification.
5. **Origin and Referer verification** as defense in depth: reject state-changing requests when the source origin cannot be established as trusted, unless another defense such as a valid token succeeds.
6. **`SameSite` cookies**, also as defense in depth.

### What `SameSite` does and does not do

| Value | Behavior (from OWASP) |
|---|---|
| `Strict` | Cookie is not sent with cross-site requests, including top-level navigations from external links |
| `Lax` | Cookie is sent on cross-site top-level navigations that use safe methods, such as following a link |
| `None` | Cookie is sent in cross-site contexts and requires the `Secure` attribute |

OWASP is direct that `SameSite` "does not replace a proper CSRF defense in most deployments", and allows that in narrow deployments it can suffice only if several conditions all hold. Its guidance is to treat it as a layer combined with a token or double-submit pattern. Two more rules from the same page:

- **Never change state with a `GET`.** If any state-changing operation is reachable by `GET`, `SameSite=Lax` will not stop it, because `Lax` allows cookies on top-level `GET` navigations. If you cannot avoid it, protect those resources against CSRF anyway.
- **The experiment above applies here too.** A simple cross-origin request reaches your server; only your own checks decide whether it is honored.

A practical combination for a cookie-session API: `SameSite=Lax` (or `Strict` where the UX allows), a CSRF token or custom header on every state-changing request, strict content-type handling, and no mutating `GET`s.

## Input validation

Everything that arrives from outside is untrusted: bodies, query strings, path parameters, headers and cookies. The OWASP Input Validation cheat sheet states the principles worth building on:

- **Define what is allowed and reject everything else.** Do not try to list every malicious string; a deny-list also rejects valid input (a name with an apostrophe) without making anything safe.
- **Validate syntax and semantics.** Syntax is the type and format; semantics is whether the value makes sense for the operation. A booking needs valid dates, and the end must be after the start.
- **Validate on the server.** Client-side checks are for user experience and can be bypassed.
- **Bound length before matching a regular expression**, and avoid patterns with heavy backtracking, to avoid regex denial of service.
- **Validation is not the defense against SQL injection.** Parameterized queries are. Validation is a separate layer.

### Validate at the edge with a schema

A schema library gives you one place that turns untrusted input into a typed value or a rejection. I tested the following with Zod 4.6.7, because several default behaviors surprised me.

**Unknown keys are silently dropped by default.** A plain object schema strips fields it does not know about:

```ts
const body = z.object({ title: z.string() })
body.parse({ title: 'a', role: 'admin', isAdmin: true })
// { title: 'a' }   <- the extra keys disappear, with no error
```

That is safe *if you use the parsed result*. The classic **mass assignment** bug is spreading the raw request body into a database write, so a caller can set fields like `role` or `ownerId`. Parse first, then use only the parsed output. If you would rather reject than ignore, use a strict object:

```ts
const strict = z.strictObject({ title: z.string() })
strict.safeParse({ title: 'a', role: 'admin' })
// success: false, issue code 'unrecognized_keys', keys ['role']
```

Rejecting is noisier but surfaces client bugs and attack attempts instead of hiding them.

**Coercion can turn garbage into valid-looking values.** Query strings are always strings, so people reach for coercion, and I found its defaults dangerous:

```ts
z.coerce.number().parse('')          // 0      (an empty string becomes zero)
z.coerce.number().parse(null)        // 0
z.coerce.boolean().parse('false')    // true   (any non-empty string is true)
z.number().safeParse('5').success    // false  (strict number rejects the string)
```

An empty `?limit=` becoming `0`, or `?active=false` becoming `true`, is a bug that validation was supposed to prevent. Constrain coerced values so nonsense fails:

```ts
const page = z.object({
  limit: z.coerce.number().int().min(1).max(100).default(20),
})
page.parse({})                       // { limit: 20 }
page.safeParse({ limit: '1000000000' }).success  // false: above the cap
page.safeParse({ limit: 'abc' }).success         // false
```

For booleans from query strings, parse the exact strings you accept (`'true'` / `'false'`) rather than coercing.

**Also validate what you do not parse.** Check the `Content-Type` and enforce a body-size limit before reading the body, as covered earlier. Limit string lengths (`z.string().max(...)`) so one field cannot carry a megabyte.

## Security headers for an API

Headers are a cheap way to turn off risky browser behavior. The OWASP HTTP Headers cheat sheet lists values; here are the ones that matter for a JSON API, with its recommended settings:

| Header | Recommended value | Why |
|---|---|---|
| `Content-Type` | Accurate, such as `application/json` | A wrong or missing type lets browsers guess |
| `X-Content-Type-Options` | `nosniff` | Stops MIME-type sniffing |
| `Cache-Control` | `no-store` on sensitive responses | Tells compliant caches not to keep them (it does not control an application-level cache) |
| `Strict-Transport-Security` | `max-age=63072000; includeSubDomains; preload` | Forces HTTPS; OWASP warns that a long max-age plus an expired certificate can lock users out |
| `Content-Security-Policy` | `default-src 'none'`; `frame-ancestors 'none'` | Useful if the API ever returns HTML; `frame-ancestors` replaces `X-Frame-Options` |
| `Referrer-Policy` | `strict-origin-when-cross-origin` (or `no-referrer` for APIs) | Limits leaked URLs |
| `Permissions-Policy` | Disable unused features, such as `geolocation=(), camera=(), microphone=()` | Stops injected scripts from enabling them |

Two to remove or avoid, per the same sources: **`X-Powered-By`** and an informative **`Server`** value (they advertise your stack), and **`X-XSS-Protection`**, which OWASP says not to use (or to set to `0`) because it can introduce vulnerabilities in otherwise safe sites. The preload flag in the HSTS value is a commitment that is hard to undo, so add it deliberately, not by copy-paste; that caution is mine, not OWASP's.

## Errors and status codes are part of the API

Attackers read your error messages, and clients depend on your status codes. The OWASP REST cheat sheet lists the codes that matter for security:

| Code | Use |
|---|---|
| `400` | Malformed request, such as a body format error |
| `401` | Missing or wrong authentication |
| `403` | Authenticated but not permitted |
| `405` | Method not on the allow-list for this endpoint |
| `406` | No acceptable response representation |
| `413` | Request body over the limit |
| `415` | Unsupported content type |
| `429` | Too many requests |
| `500` | Server error; must not reveal internal details or stack traces |

Respond with generic messages and keep technical detail in your logs, not in responses. A consistent error shape helps clients, and there is a standard for it: RFC 9457, *Problem Details for HTTP APIs* (which obsoletes RFC 7807), defines the `application/problem+json` media type with the members `type`, `status`, `title`, `detail` and `instance`.

```http
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 30

{
  "type": "about:blank",
  "status": 429,
  "title": "Too many requests",
  "detail": "Try again in 30 seconds."
}
```

Remember the tradeoff from Part 5: whether a record you may not see returns `404` or `403` is a deliberate choice, since `403` confirms the record exists.

## Testing it yourself

You can check most of this from a terminal. Use a staging environment and keep your volume modest.

**Does your API allow the wrong origin?** Send a preflight and read the response headers:

```bash
curl -si -X OPTIONS https://api.example.com/orders \
  -H 'Origin: https://evil.example' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type' | head -20
```

You should not see `Access-Control-Allow-Origin: https://evil.example`. Also try `Origin: null`.

**Does the API honor a mislabeled body?**

```bash
curl -si -X POST https://api.example.com/orders \
  -H 'Content-Type: text/plain' -d '{"item":1}' | head -1
# you want 415 (unsupported media type), not 200
```

**Does the rate limit work, and on what key?**

```bash
for i in $(seq 1 12); do
  curl -s -o /dev/null -w '%{http_code}\n' -X POST https://staging.example.com/api/login \
    -H 'content-type: application/json' -d '{"email":"a@example.com","password":"x"}'
done | sort | uniq -c
```

Then repeat with a different `X-Forwarded-For` header to confirm the limit does not trust it.

**What headers does a response carry?**

```bash
curl -sI https://api.example.com/health | grep -i -E 'x-powered-by|server|strict-transport|x-content-type|cache-control'
```

**Does validation reject what it should?** Try an unknown field, an empty `limit`, `limit=1000000000`, a string where a number belongs, and a body larger than your limit, and confirm you get a `4xx` each time, never a `500`.

**Production checklist:**

- Rate limits on sensitive flows, keyed by a trustworthy identity; shared state across instances; `429` with `Retry-After`.
- Caps on body size, page size, query cost and request time.
- CORS allow-list of exact origins; no wildcard with credentials; no blind origin reflection; `Vary: Origin`.
- Strict content-type handling with `415`; no JSON parsing of mislabeled bodies.
- CSRF defense on every state-changing route that uses cookie auth; no mutating `GET`.
- Schema validation at the edge; parsed output used, never the raw body; constrained coercion.
- Parameterized queries; allow-listed outbound destinations.
- `nosniff`, accurate `Content-Type`, `no-store` where sensitive, HSTS, and no stack or version headers.
- Generic, consistent error responses; the right status code each time.
- An inventory of endpoints and versions, including the forgotten ones.

## Interview answer vs production answer

The interview answer: *"Use rate limiting, enable CORS properly, protect against CSRF with tokens, validate input, and set security headers."*

The production answer adds what makes each of those real:

- **CORS is not a firewall.** My own test showed a blocked cross-origin request reaching the server and running. Whether the *script* may read the answer is a separate question from whether the request is honored.
- **The content type is a security boundary.** It decides whether a request is preflighted at all.
- **`SameSite` is a layer, not the defense.** OWASP says so explicitly, and mutating `GET`s bypass `Lax`.
- **Validation output is the only trusted data.** Dropping unknown keys silently and coercing empty strings to zero are defaults you must understand before you trust them.
- **The limit is only as good as its key.** Trusting a client-supplied forwarding header, or keeping counters in one instance's memory, quietly removes the protection.
- **Quiet failures are part of the design.** Decide what happens when your limiter's store or your validation dependency is down.

## FAQ

### Does CORS protect my API from other websites?
Only in the narrow sense that browsers will not let another site's script read your responses unless you allow it. It does not stop requests from being sent, it does not stop non-browser clients, and in my test a blocked request still reached the server. Use authentication, authorization and CSRF protection for the actual defense.

### Why does my request work in Postman or `curl` but fail in the browser?
Because the browser enforces CORS and those tools do not. The server's response is missing the CORS headers the browser needs to expose it to your script.

### Do I need CSRF protection if I use JWTs?
It depends on where the token lives. A token your JavaScript attaches to an `Authorization` header is not sent automatically by the browser, so a hostile page cannot ride on it. A token stored in a cookie is sent automatically and needs CSRF protection like any cookie session. Part 2 covers the storage tradeoffs.

### Is `SameSite=Lax` enough?
OWASP says it is not a replacement for a proper CSRF defense in most deployments and should be combined with a token or double-submit pattern. It also does not help against state-changing `GET` requests.

### Should I use `X-Forwarded-For` for rate limiting?
Only the value appended by a proxy you control. The header can be set by the caller, so trusting its first value lets an attacker change their apparent address on every request.

### What is mass assignment?
Writing a request body directly into a record, so a caller can set fields you never meant to expose, such as `role` or `ownerId`. Avoid it by validating against a schema and using only the validated output.

### What is the difference between a `401` and a `403`?
`401` means the caller is not authenticated (missing or wrong credentials). `403` means they are authenticated but not allowed to do this.

### Is the `RateLimit` header standard?
Not yet. When I checked, it was an active IETF Internet-Draft rather than an RFC, so treat it as emerging. `429` and `Retry-After` are the established parts.

## Glossary

| Term | Simple meaning |
|---|---|
| Origin | Scheme + host + port of a page or API |
| CORS | Browser rules about which origins' scripts may read a response |
| Preflight | An `OPTIONS` request the browser sends before a non-simple cross-origin request |
| Simple request | A cross-origin `GET`, `HEAD` or `POST` with only safelisted headers and content types |
| CSRF | Tricking a signed-in browser into sending an unwanted request |
| Synchronizer token | A per-session random value required on state-changing requests |
| Double-submit cookie | A token sent both as a cookie and in the request, and compared |
| `SameSite` | Cookie attribute limiting when cookies are sent on cross-site requests |
| Fetch Metadata | Request headers such as `Sec-Fetch-Site` describing where a request came from |
| Rate limit | A cap on how much one caller can do in a period |
| Token bucket | A rate algorithm that refills tokens steadily and spends one per request |
| Mass assignment | Writing unvalidated request fields straight into a record |
| HSTS | A header telling browsers to use HTTPS only |
| Problem Details | RFC 9457 standard JSON error format |

## The end-to-end mental model

```txt
 request from the internet
          |
          v
 TLS terminates; security headers added on the way out
          |
          v
 RATE LIMIT  (key: trusted identity; caps on body, page size, cost)  -> 429 + Retry-After
          |
          v
 BROWSER-ORIGIN CHECKS
   CORS    : who may READ the response     (browser-enforced; not access control)
   CSRF    : was this request intended?    (token / custom header / Sec-Fetch-Site / Origin)
          |
          v
 PARSE + VALIDATE
   content type allowed?  -> 415
   size within limit?     -> 413
   schema (allow-list)    -> 400 ; use parsed output only
          |
          v
 AUTHENTICATE (Parts 2-4)  ->  AUTHORIZE (Part 5)  ->  handler
          |
          v
 generic, consistent errors (RFC 9457); details stay in the logs
```

Part 5 gave the system a rulebook; this part guarded the entrance. Neither helps much if, when something does go wrong, you cannot tell what happened. The next step in the series is what you do after the request is handled: error handling, logging and observability in Node.js backends.
