The Backend Security Checklist
The controls every service owes no matter what it does, and the layer each one has to live in.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind has a problem.
What does every backend service owe, independent of what it is for?
A new service is going to production next week. Someone has to say what "secure enough to ship" means without turning it into a six-month programme.
We will get a penetration test before launch and fix what it finds. Security is a phase near the end, done by people who specialise in it.
A pentest finds what it can reach in five days. It will not find the object-level authorization gap on the endpoint nobody demoed, and that is the one that leaks every tenant.
- A pentest finds what it can reach in five days. It will not find the object-level authorization gap on the endpoint nobody demoed, and that is the one that leaks every tenant.
- Findings arrive after the design is frozen, so the fixes are patches on top of a structure that made the bug easy — the same class reappears in the next endpoint.
- Nothing in the list is expensive when it is a habit. Everything in it is expensive when it is a remediation ticket six weeks before an audit.
- The controls that matter most — authorization on every object, secrets never logged, parameterized queries everywhere — are enforced by how the code is written, not by a report.
What is actually happening
- Almost every serious backend vulnerability is one of two failures: input was trusted or the caller was not checked against the object. Injection, SSRF, path traversal and mass assignment are the first. IDOR, tenant leakage and privilege escalation are the second.
- Each control belongs at a specific layer, and putting it at the wrong one makes it decorative. A shape check at the edge cannot enforce a business rule; a gateway token check cannot enforce ownership of row 4711.
- Controls compose multiplicatively when they are independent and additively when they are the same idea in two places. Two validation libraries are one control; a parameterized query plus a read-only database user are two (Defence in Depth).
- The checklist is short because it is the intersection of "every service needs it" and "the application is the only place it can be enforced". Anything a platform genuinely handles is not on it.
Eleven controls, and the layer each one lives in
The value of the list is not the items — most engineers can name them. It is the second column: the layer where the control is actually enforceable. A control implemented one layer too high is a control that a single new code path bypasses.
Read the third column as the specific consequence. "Insecure" is not a consequence; "any authenticated user reads any tenant's invoices by changing one integer" is, and it is the one that gets written up.
| Control | Where it has to be enforced | What an attacker gets without it |
|---|---|---|
| Authentication | Middleware, before any handler, with public routes declared explicitly | Every endpoint as an anonymous caller |
| Authorization | At the point the object is loaded, per object, from the session principal | Every other user's and tenant's data by changing an id (Object-Level Authorization) |
| Input validation | The process boundary, converting to domain types | Whatever the downstream sink does with unexpected input (The Trust Boundary) |
| Secrets | Runtime configuration and a secret store — never image, repo or log | Your database, your provider accounts, your signing keys (Secrets Are Not Configuration) |
| SQL injection | The query call site: parameters, never concatenation | Read or write of everything that database user can reach (SQL Injection) |
| Command injection | The process-spawn call site: argument arrays, no shell | Code execution as your service account (Command Injection) |
| SSRF | The HTTP client plus network egress policy | Your internal network and cloud credentials from the metadata endpoint (SSRF — When the Backend Fetches a URL) |
| File uploads | Handler and storage layer: size, content-derived type, server-generated key | Storage overwrite, path traversal, or a payload served back to other users (File Uploads Through the Backend) |
| Rate limiting | Before authentication's expensive part, keyed on a stable identity | Credential stuffing and enumeration at whatever rate they can send (Rate Limiting) |
| Logging discipline | The logging call site, with redaction as a second layer | Credentials and personal data spread across every downstream log system (Secrets in Logs) |
| Dependency security | Lockfile, CI, and a patch process with a known latency | A published exploit against a version you are still running (Dependency Security) |
The request path, annotated with where each control sits
Ordering is a correctness property here, not a style choice. Rate limiting after password verification means the expensive part runs for every attempt. Authorization before the object is loaded means the check runs against an id rather than a record, which is how ownership checks quietly become nothing.
The step that surprises people is the last one. Output is a control surface: error responses leak stack traces and internal hostnames, and serialization leaks fields the client was never meant to see (Not Leaking Your Internals, Three Models, Not One).
- 1Edge limits
Body size, header size, connection and request caps.
fails by Defaults treated as tuned values; one large upload exhausting an instance (Request Bodies and Streaming).
- 2Rate limit
Bounds attempts before expensive work.
fails by Placed after authentication, or keyed on an unfiltered forwarding header the client controls.
- 3Authenticate
Establishes the principal or rejects.
fails by A route added outside the pipeline; a token verified for signature but not for audience or expiry (Token Authentication and the Revocation Problem).
- 4Validate + parse
Turns untrusted bytes into checked domain values.
fails by Body validated, query and headers not; the raw request passed deeper anyway.
- 5Load object
Fetches the record the request names.
fails by Loading by id alone rather than by id scoped to the principal's tenant.
- 6Authorize
Decides whether this principal may do this to this object.
fails by Role checked, ownership not; check in a
SELECTbut not in the followingUPDATE. - 7Act
Business logic: queries, subprocesses, outbound fetches.
fails by Concatenated SQL, shell strings, unrestricted outbound URLs.
- 8Respond + log
Serializes a result and records what happened.
fails by Internal errors echoed to the client; the whole request object handed to the logger (Secrets in Logs).
The two omissions that account for most of the damage
Injection is the famous one, but modern query APIs make it hard to get wrong by accident. The two failures that show up again and again in real incident reports are an authorization check that never looked at the object, and a credential that ended up in a log.
They share a property that makes them dangerous: the service behaves perfectly. There is no error, no alert, no latency change. The first is discovered when someone changes an id out of curiosity; the second when a credential appears in a log export or a support ticket.
if (!session.roles.includes('member')) return res.status(403).end()
const invoice = await invoices.findById(req.params.id)
return res.json(invoice)const invoice = await invoices.findOne({
id: req.params.id,
tenantId: session.tenantId, // from the verified session, never the body
})
if (!invoice) return res.status(404).end()
return res.json(toInvoiceDto(invoice))The first answers "is this caller a member of something", which every customer is. The second answers "is this caller a member of the thing that owns row 4711", which is the actual question. Scoping the load means there is no window between check and use, and returning 404 rather than 403 avoids confirming that the id exists.
How to build it
Most important first.
- Authentication — every request establishes a principal, or is explicitly public. "Public" is a decision recorded in code, not a route somebody forgot (Authentication in a Backend).
- Authorization — a check per object, at the point the object is loaded, derived from the authenticated principal and never from a request field (Object-Level Authorization, Where the Check Belongs).
- Input validation — every surface, converted to a domain type at the boundary: body, query, path, headers, cookies, file contents and names (The Trust Boundary, Parse, Do Not Validate).
- Secrets — out of the repository, out of the image, out of the logs, rotatable without a deploy (Secrets Are Not Configuration, Secrets in Logs).
- SQL injection — parameterized queries with no exceptions, and an allow-list for the parts of a query that cannot be parameters (SQL Injection).
- Command injection — no shell. Argument arrays, or a library instead of a subprocess (Command Injection).
- SSRF — no unrestricted backend fetch of a user-supplied URL; egress control at the network, not a string blocklist (SSRF — When the Backend Fetches a URL).
- File uploads — size limits, type decided by content not by the client, storage keys the server generates, and never a path built from a supplied name (File Uploads Through the Backend).
- Rate limiting — applied before the expensive work, keyed on something the caller cannot rotate freely (Rate Limiting, Authenticate First, or Rate-Limit First?).
- Logging — enough to reconstruct who did what, with credentials and personal data excluded by construction (Structured Logging, Secrets in Logs).
- Dependency security — a committed lockfile, reproducible installs, and a known time-to-patch (Dependency Security).
What can go wrong
- The checklist becomes a spreadsheet signed off once, describing the service as it was three quarters ago.
- A control is implemented in middleware and then bypassed by an internal route, a batch endpoint or an admin tool that does not go through the pipeline (Middleware Ordering Is a Correctness Decision).
- Rate limiting placed after authentication, so unauthenticated brute force still costs a full credential verification per attempt.
- Validation that rejects bad shapes and then hands the raw request object deeper anyway, so the checked value and the used value are different objects.
- A control that fails open: the authorization service times out and the request proceeds (Fail Open vs Fail Closed in Security Engineering).
- Check-then-act on a permission is a TOCTOU gap: a role revoked between the authorization check and the write still allows the write. Enforce ownership in the statement — a
WHERE tenant_id = $1on the update — rather than only in a priorSELECT(Object-Level Authorization). - Rate limit counters incremented non-atomically under concurrency let a burst through the limit that a serial trace would have blocked (Atomic Operations).
- The two most damaging omissions are consistently object-level authorization and secrets in logs. Both are invisible in normal operation, and both are total when they fail — one leaks every tenant, the other leaks a credential into every downstream log system.
- A missing control is not a probability, it is a standing invitation: the endpoint without an ownership check does not fail sometimes, it fails every time someone changes the id.
- Everything reachable is in scope, including the health endpoint, the metrics endpoint, the admin tool and the internal API on the private network (Attack Surface in Security Engineering).
- "We have authentication, so we have access control." Authentication says who is calling. Authorization says what they may touch. They are separate systems and the second is the one that leaks data (Authentication vs Authorization).
- "The WAF covers injection." A WAF sees patterns in requests. It does not know that this user may not read that invoice, and it cannot see the query your ORM builds.
- "It is an internal service, so most of this does not apply." Internal describes the network path. It says nothing about the data, and lateral movement is the normal shape of a real incident.
- "Security is the security team's job." Everything on this list is enforced in application code, which the security team does not write.
Operating it
- Count authorization denials by principal and endpoint. Zero denials on an endpoint that has an ownership check usually means the check is unreachable, not that everyone is well behaved.
- Count validation rejections by field and by client. A single client suddenly failing on one field is a broken deploy; many clients failing on many fields is probing.
- Alert on authentication failures per principal and per source, not in aggregate — the aggregate hides a targeted attempt inside normal noise.
- Track time-to-patch for dependency advisories as a number your team can say out loud. It is the only honest measure of that control.
- Nothing on the list gets less necessary with growth, but enforcement mechanism changes: at five endpoints a code review holds the line; at five hundred it has to be a type, a lint rule or a test.
- More services means the controls have to be inherited rather than reimplemented — a shared middleware package, a service template, a CI check — or the newest service is always the weakest.
- At scale the interesting question stops being "is the control present" and becomes "how would we know if it were removed". That question is answered by tests and metrics, not by documentation.
- Strict validation and per-object authorization add code to every handler and latency to every request. The alternative is a smaller codebase with a class of bug you cannot review your way out of.
- Enforcing controls structurally — a repository API that cannot be called without a tenant, a query builder that cannot interpolate — costs flexibility. That is the point, and it is a real cost when a legitimate case needs the escape hatch.
- A checklist invites the belief that finishing it means done. It is a floor, and floors are not ceilings.
Where this applies
Backend advice is context-sensitive. These labels say what each claim is specific to, and where a different stack or scale would differ.
- GENERALThe list holds for any backend exposed to any caller. What differs by stack is the enforcement mechanism — a type system, a lint rule, a middleware — not the requirement.
- SCALE-SPECIFICBelow roughly one team, review and habit enforce these. Above it, anything not enforced by a test, a type or a CI check drifts within a quarter, because the person who knew the rule is no longer in every pull request.
Where the depth lives
This domain teaches the application-side mechanism and hands the rest off.
- — Testing & Reliability Engineering — a security control without a test that fails when it is removed is a comment.