The Trust Boundary
Everything crossing into your process is untrusted — including responses from services you own.
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.
Which inputs are untrusted, and where exactly does trust begin?
The service accepts data from browsers, mobile apps, partner systems, webhooks and its own sibling services. Someone has to decide what gets checked.
Validate what users type. Data from our own services, and fields our frontend controls, are fine.
Your frontend is not the only client. curl, a stale mobile build and a partner integration all reach the same endpoint.
- Your frontend is not the only client. curl, a stale mobile build and a partner integration all reach the same endpoint.
- An internal service can be compromised, misconfigured, or simply buggy — and it now sends you a malformed payload you never validate.
- Response data from a third party lands in your database and later in someone's browser. An unvalidated external response is a stored injection vector.
- Headers, cookies and file metadata are inputs that validation code routinely skips.
What is actually happening
- The trust boundary is the process edge. Bytes arriving from outside carry no guarantees regardless of who sent them.
- Trust is not transitive: service A validating its input does not mean A's output to B is valid, because A may have transformed it, and B may need different constraints.
- Validation converts untrusted input into a typed, checked value. After that point, code can rely on it — which is the whole reason to do it at a boundary rather than everywhere (Parse, Do Not Validate).
Every surface, not just the body
Validation libraries encourage thinking about "the request body" because that is what they schema-check most naturally. Attackers do not share that focus.
- Body — the surface everyone validates.
- Query parameters — reach the same code, often unvalidated, frequently interpolated into filters and sorts.
- Path parameters — usually ids, and usually the object-level authorization decision (Object-Level Authorization).
- Headers — including forwarding headers used for rate limiting and audit.
- Cookies — attacker-controlled unless signed.
- Uploaded files — contents *and* names *and* declared type (File Uploads Through the Backend).
- Webhook payloads — unauthenticated HTTP from the internet until a signature is verified (Webhook Signature Verification).
- External API responses — data you asked for is still data you did not write.
Claims, not facts
The single most useful reframing in this lesson: a request field is a claim. {"tenantId": "acme"} does not mean the caller belongs to Acme. It means the caller asserts they do.
Identity and tenancy must come from the authenticated principal — the session, the token's verified claims, the API key's owner record. A field that also appears in the body is at best redundant and at worst the vulnerability.
// tenantId comes from the request body const rows = await db.query( 'SELECT * FROM invoices WHERE tenant_id = $1', [body.tenantId], )
// tenantId comes from the verified session const rows = await db.query( 'SELECT * FROM invoices WHERE tenant_id = $1', [session.tenantId], )
The first is parameterized, so it is safe from SQL injection — and still lets any authenticated user read any tenant's invoices by changing one field. Injection safety and authorization are different problems, and fixing one does not touch the other.
How to build it
Most important first.
- Enumerate every input surface: body, query string, path parameters, headers, cookies, uploaded file contents *and* names, webhook payloads, and external API responses.
- Validate at the boundary and convert to a domain type. Do not pass raw request objects deeper into the application.
- Validate external API responses against the shape you expect, rather than trusting a provider's documentation.
- Treat internal service calls as external ones at the boundary, even if the network is private.
What can go wrong
- Validating the body but not query parameters, which reach the same SQL.
- Trusting a filename and using it as a storage key or path (File Uploads Through the Backend).
- Trusting
Content-Typeto decide how to parse. - Trusting an id in the request to identify the tenant, rather than deriving it from the authenticated session (Tenant Isolation).
- Validation followed by a separate action is a time-of-check/time-of-use gap: a record valid when checked may be gone when used. Enforce in the database where it matters (Database Constraints).
- The most damaging backend vulnerabilities are trust-boundary failures: injection, SSRF, IDOR and tenant leakage all come from treating input as trustworthy (SSRF — When the Backend Fetches a URL, SQL Injection, Object-Level Authorization).
- Never derive identity or tenancy from a client-supplied field. Derive them from the authenticated principal.
- Client-supplied ids are requests, not facts: "update order 123" means "I claim I may update 123".
- "It is internal, so it is trusted." Internal describes the network path, not the data.
- "The frontend validates it." The frontend improves the user experience; it cannot enforce anything.
- "It is from a reputable provider." Reputable providers ship bugs and get compromised, and your parser meets the payload either way.
Operating it
- Count validation rejections by field and by caller. A spike is either a broken client or someone probing.
- Log rejected input shape without logging the values, which may be credentials or personal data (Secrets in Logs).
- Validation cost is proportional to payload size — a concern only when payloads are large or rates are high (What Serialization Costs).
- More clients means more variants of "wrong but sincere" input; strictness that was fine internally becomes an integration burden.
- Strict validation breaks clients on changes that a lenient parser would tolerate — good for correctness, a real cost for a public API (Running Two API Versions in One Service).
- Validating internal calls duplicates work and adds latency. It also contains the blast radius of one buggy service.
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.
- GENERALUniversal. This is one of the few backend claims with no legitimate exception.
Where the depth lives
This domain teaches the application-side mechanism and hands the rest off.