#Trust boundaries and the OWASP Top 10
A trust boundary is any point where data or control passes from something you don't control into something you do: the internet into your load balancer, a request body into your SQL, a user's comment into another user's browser, a query string into a shell. Nearly every vulnerability in this chapter is the same mistake made at a different boundary: data from the untrusted side was treated as if it came from the trusted side — as SQL, as shell syntax, as JavaScript, as an object's prototype, as an admin flag, or as a request the user really meant to make.
A good mental model is airport security. There isn't one checkpoint; there are several, and each one checks a different thing. Your passport proves who you are (authentication). Your boarding pass says which flight you may board (authorisation). The bag scanner checks what you carry (input validation). Showing a valid passport doesn't let you onto any plane you like, and a clean bag doesn't prove who you are. Web security works the same way, and the classic interview mistake is to name one tool — "we use JWT", "we use Helmet" — as if it covered every checkpoint.
Internet ─▶ edge / gateway ─▶ TLS ─▶ rate limits ─▶ Express
─▶ authentication (who?) ─▶ authorisation (may they, on THIS object?)
─▶ validation (is the input acceptable?) ─▶ business logic
─▶ safe interpreter APIs (SQL params, execFile, no eval) ─▶ response
─▶ browser controls (output encoding, CSP, cookies, CORS)- Transport (TLS) stops people on the network from reading or changing traffic. It does nothing about what the authenticated client itself sends.
- Rate limits protect limited resources (CPU, password guesses, money) from volume. They don't care whether each request is well-formed.
- Authentication answers "who is this?" and authorisation answers "may they do this to this particular object?". They are separate checks; skipping the second is the most common real-world bug (below).
- Validation checks that a value is acceptable (type, length, range, allowed fields). Safe interpreter APIs make sure even an acceptable value can never become code — parameters, argument arrays, no
eval. You need both, because they solve different problems. - Browser controls run in someone else's browser, so the server can only ask for them with headers and cookie attributes. They protect your users from other websites and from injected script, not your server from clients like
curl.
The OWASP Top 10 is the industry's ranked list of the most common web-application risk categories. My notes said to know it; here is the current edition (2025) with the Node angle. Many prep guides still quote the 2021 numbering, where Injection was A03 and SSRF had its own entry (it is now folded into Broken Access Control), so say which edition you mean.
| 2025 | Category | What it looks like in a Node service | Where |
|---|---|---|---|
| A01 | Broken Access Control | GET /users/100/orders works with user 42's token; SSRF | below, Mass assignment |
| A02 | Security Misconfiguration | stack traces in responses, cors({ origin: true, credentials: true }), default headers | Helmet, CORS |
| A03 | Software Supply Chain Failures | a compromised or abandoned npm package, no lockfile, install scripts | Checklist |
| A04 | Cryptographic Failures | plain HTTP, weak password hashing, secrets in the repo | HTTPS, bcrypt |
| A05 | Injection | SQL, NoSQL, shell, eval, XSS | the next four topics, XSS |
| A06 | Insecure Design | a reset flow that can be brute-forced by design | — |
| A07 | Authentication Failures | no lockout or rate limit on /login, long-lived tokens | Authentication chapter |
| A08 | Software or Data Integrity Failures | trusting unsigned data, prototype pollution via unsafe merges | Prototype pollution |
| A09 | Security Logging and Alerting Failures | no audit log of failed logins or admin actions | Logging |
| A10 | Mishandling of Exceptional Conditions | errors that fail open, leak internals or crash the process | Checklist |
Number one is access control, and it's the one no library fixes for you. A valid token for user 42 tells you who is calling; it says nothing about whether they may read user 100's orders.
const express = require("express");
const request = require("supertest");
const orders = [
{ id: 1, userId: 42, item: "keyboard" },
{ id: 2, userId: 100, item: "monitor" },
];
const app = express();
// Authentication: who is calling? (stands in for verifying a JWT — see chapter 08)
app.use((req, res, next) => {
const token = req.get("authorization");
if (token === "Bearer token-for-42") req.user = { id: 42, role: "user" };
else if (token === "Bearer token-for-admin") req.user = { id: 1, role: "admin" };
else return res.status(401).json({ error: "not authenticated" });
next();
});
// ❌ Authenticated, but never asks "may THIS user read THESE orders?"
app.get("/v1/users/:id/orders", (req, res) => {
res.json(orders.filter((o) => o.userId === Number(req.params.id)));
});
// ✅ Authorisation: object ownership, checked on the server
app.get("/v2/users/:id/orders", (req, res) => {
const ownerId = Number(req.params.id);
if (req.user.id !== ownerId && req.user.role !== "admin") {
return res.status(403).json({ error: "forbidden" });
}
res.json(orders.filter((o) => o.userId === ownerId));
});
(async () => {
const as42 = (path) => request(app).get(path).set("Authorization", "Bearer token-for-42");
let r = await request(app).get("/v1/users/100/orders");
console.log(r.status, r.body); // 401 { error: 'not authenticated' }
r = await as42("/v1/users/100/orders");
console.log(r.status, r.body); // 200 [ { id: 2, userId: 100, item: 'monitor' } ]
r = await as42("/v2/users/100/orders");
console.log(r.status, r.body); // 403 { error: 'forbidden' }
r = await as42("/v2/users/42/orders");
console.log(r.status, r.body); // 200 [ { id: 1, userId: 42, item: 'keyboard' } ]
})();- The first middleware is the authentication checkpoint. With no
Authorizationheader it returns401and the route never runs — so the first request prints401 { error: 'not authenticated' }. 401 means "I don't know who you are". - The second request carries user 42's token, so
req.userbecomes{ id: 42, role: "user" }andnext()passes control to/v1/users/:id/orderswithreq.params.id === "100". - The v1 handler filters by the id in the URL, which the caller chose. It never compares it with
req.user.id, so user 42 gets user 100's monitor order:200 [ { id: 2, … } ]. This is an IDOR (insecure direct object reference): just change the number in the URL. - The v2 handler compares the owner in the URL with the identity the server verified:
42 !== 100and the role isn'tadmin, so it returns403— "I know who you are, and the answer is no". - Asking for their own orders (
/v2/users/42/orders) passes the check and returns the keyboard. The check uses only server-side facts (req.usercame from verifying the token), never a client-suppliedroleorisAdmin. - Note that
Number(req.params.id)matters: params are strings, and42 !== "42"would lock users out of their own data. Some APIs return404instead of403so outsiders can't even confirm the resource exists.
An Express API has Helmet, a strict CORS allowlist, rate limiting and JWT authentication. User 42, with a valid token, sends DELETE /users/100 from curl. Which of those four controls stops the request?
Show answer
None of them. Only an authorisation check in the handler (or a policy layer) stops it.
- Helmet sets response headers that instruct browsers.
curlignores them, and they don't affect what the server does with the request anyway. - CORS is enforced by browsers, deciding whether a page's JavaScript may read the response.
curlisn't a browser and sends noOrigin, so CORS never comes into play. - Rate limiting counts requests. One delete request is well under any limit.
- JWT authentication succeeds — the token is valid and really belongs to user 42. That's exactly the problem: authentication answered "who", and nobody asked "may 42 delete 100?".
- The fix is the ownership/role check from the v2 route above, applied to every route that touches an object by id.