# Request Handling

Everything that crosses the boundary is untrusted until a schema says
otherwise: body, query, path, headers, cookies, uploaded files, and responses
from other services.

## Contents

1. Validate at the edge with a schema
2. Writable fields only (mass assignment)
3. Shape every response explicitly
4. Injection
5. Files and uploads
6. Fetching user-supplied URLs (SSRF)
7. Errors that do not leak
8. Consuming third-party APIs

## 1. Validate at the edge with a schema

Parse, do not check. A schema turns unknown input into a typed value or a 400,
and the handler only ever sees the typed value.

TypeScript with Zod:

```ts
import { z } from "zod";

const CreateOrder = z.object({
  productId: z.string().regex(/^p_[a-z0-9]{6,24}$/),
  quantity: z.number().int().min(1).max(100),
  note: z.string().trim().max(500).optional(),
}).strict();

app.post("/orders", async (req, reply) => {
  const parsed = CreateOrder.safeParse(req.body);
  if (!parsed.success) return reply.code(400).send(validationProblem(parsed.error));
  const order = await orders.create(req.auth, parsed.data);
  return reply.code(201).send(toOrderView(order));
});
```

Python with Pydantic v2:

```python
from pydantic import BaseModel, ConfigDict, Field

class CreateOrder(BaseModel):
    model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
    product_id: str = Field(pattern=r"^p_[a-z0-9]{6,24}$")
    quantity: int = Field(ge=1, le=100)
    note: str | None = Field(default=None, max_length=500)
```

Guidelines:

- Bound every string, number and array. Unbounded input is a resource risk.
- Prefer allowlists (enums, patterns) over deny lists.
- Validate path and query parameters too, not just the body.
- Normalise before validating (trim, Unicode NFC) and validate before use.
- Reject unknown fields (`strict()`, `extra="forbid"`) on write endpoints, or at
  minimum drop them; never pass them through.

## 2. Writable fields only (mass assignment)

The failure: `await db.user.update({ where: { id }, data: req.body })`. The
client adds `"role": "admin"` and the ORM obliges.

Fix by construction:

- A separate input schema per operation (`CreateNote`, `UpdateNote`), listing
  only fields the caller may set.
- Server-owned fields (`id`, `ownerId`, `createdAt`, `status`, `price`) are set
  by the server from context, never read from input.
- Different roles, different schemas: an admin update schema may allow
  `status`; the customer schema does not include it at all.

## 3. Shape every response explicitly

```ts
export function toOrderView(o: OrderRow) {
  return {
    id: o.id,
    status: o.status,
    items: o.items.map((i) => ({ productId: i.productId, quantity: i.quantity })),
    total: { amount: o.totalMinor, currency: o.currency },
    createdAt: o.createdAt.toISOString(),
  };
}
```

- Never return ORM entities or `SELECT *` rows; new columns then leak
  automatically.
- Filtering sensitive fields in the client is not a control.
- In FastAPI use `response_model`; in NestJS use serialisation groups or
  explicit DTO mapping; in GraphQL expose only fields with resolvers you wrote.

## 4. Injection

**SQL.** Parameterised queries or a query builder, always. String building with
user values is a defect even when "it's only an integer".

```ts
await sql`SELECT id, status FROM orders WHERE account_id = ${accountId} AND id = ${orderId}`;
```

Dynamic identifiers (sort column, direction) come from an allowlist map, never
from input directly:

```ts
const SORTABLE = { created: "created_at", total: "total_minor" } as const;
const column = SORTABLE[req.query.sort as keyof typeof SORTABLE] ?? "created_at";
```

**NoSQL.** Reject objects where scalars are expected; `{ "$ne": null }` in a
MongoDB filter bypasses equality checks. Schema validation handles this.

**Command execution.** Avoid shells. Use `execFile` / `subprocess.run([...])`
with an argument array and no `shell=True`.

**Templates and HTML.** APIs usually return JSON, but anything rendered later
(emails, PDFs, admin dashboards) must escape by context. Use the template
engine's auto-escaping; sanitise rich text with a maintained sanitiser
(DOMPurify, `nh3`).

**Log injection.** Structured JSON logging escapes newlines; avoid string
concatenation of user input into log lines.

## 5. Files and uploads

- Cap size at the proxy and in the app. Stream to storage; do not buffer whole
  files in memory.
- Decide type from content (magic bytes), not the filename or `Content-Type`.
- Store outside the web root, under a server-generated name, in object storage
  with private ACLs; serve through short-lived signed URLs.
- Scan for malware where users share files with each other.
- Re-encode images to strip metadata (location data in EXIF) and neutralise
  malformed payloads.
- Archives: limit entry count and total uncompressed size, and reject paths with
  `..` (zip slip).

## 6. Fetching user-supplied URLs (SSRF)

Any feature that fetches a URL on the caller's behalf needs a dedicated client.

1. Parse the URL; allow only `https` (and `http` only if you must).
2. Resolve the hostname yourself and reject private, loopback, link-local,
   unique-local IPv6 and cloud metadata addresses.
3. Connect to the resolved IP (to defeat DNS rebinding between check and use),
   sending the original host in SNI and `Host`.
4. Disable automatic redirects, or re-run the checks on every hop.
5. Apply a short timeout and a response size cap.
6. Run the fetcher where network policy blocks internal ranges anyway.

Libraries such as `ssrf-req-filter` (Node) or an egress proxy like Smokescreen
implement most of this; still test with `http://169.254.169.254/`,
`http://[::1]/`, decimal IP forms and a redirecting URL.

## 7. Errors that do not leak

- Return a problem-details body with a stable `type`, a generic `title`, and a
  request id. Log the stack trace server-side under that id.
- Do not echo raw input back in error messages where it could be rendered.
- Validation errors may name the field and the rule; they must not reveal
  whether a related record exists in another tenant.
- Turn off framework debug pages in every environment reachable from outside.

## 8. Consuming third-party APIs

- Validate upstream responses with the same schema tooling you use for requests.
- Set connect and read timeouts, retry only idempotent calls with backoff, and
  wrap the dependency in a circuit breaker.
- Cap response size; a compromised or buggy upstream can return gigabytes.
- Pin TLS verification on; never disable certificate checks to "make it work".
- Treat webhook payloads as untrusted until the signature verifies, then still
  validate the body.
