Claude Fable 5.1 & GPT-6 Astra packages are live

Validation

Free · MIT

Validating input at the trust boundary — schema-first parsing, allowlists, mass-assignment prevention, and separating shape from business rules.

205 lines8.3 KB Sarvam Ai Backend
Target models
Sarvam-105BSarvam-30BSarvam FamilyFuture Sarvam Models
Name
validation
Category
Backend
Description
Validating input at the trust boundary — schema-first parsing, allowlists, mass-assignment prevention, and separating shape from business rules.
License
MIT
Author
Agent.md maintainers
Last verified
2026-08-23
Reviewed by
unreviewed

#Locale

Examples use Indian conventions: ₹ amounts, IST, dd/mm/yyyy, Aadhaar and DPDP Act where a standard mentions identity or privacy law. Keep them when you copy an example.


#Purpose

Rules for validating input on the server. The principle is one line: anything that crosses a trust boundary is parsed into a known type before any code acts on it.

Client-side validation is a user-experience feature. It provides no security — the client is under the attacker's control. Every rule here is server-side.


#Parse, do not validate

Validation that returns a boolean leaves you holding the same untyped value. Parsing returns a new, typed value that cannot be wrong further down.

ts
const CreateOrder = z.object({
  customerId: z.string().uuid(),
  items: z.array(z.object({
    sku: z.string().regex(/^[A-Z0-9-]{3,32}$/),
    qty: z.number().int().min(1).max(999),
  })).min(1).max(100),
  note: z.string().max(500).optional(),
  currency: z.enum(["EUR", "USD", "GBP"]),
}).strict();                                   // reject unknown keys

app.post("/v1/orders", (req, res) => {
  const parsed = CreateOrder.safeParse(req.body);
  if (!parsed.success) return res.status(422).json(toFieldErrors(parsed.error));
  return createOrder(parsed.data);             // typed from here down
});

After this line, parsed.data.qty is a bounded integer by construction. No downstream function needs to re-check it, and the type system enforces that.

Tooling: zod/valibot/typebox (TypeScript), pydantic (Python), go-playground/validator (Go), Bean Validation (Java), dry-schema (Ruby). Where you already publish an OpenAPI document, generate the validator from it so the spec and the check cannot disagree. → API/open-api


#.strict() is the mass-assignment fix

ts
// Without .strict(): { "email": "…", "role": "admin", "credits": 999999 }
await db.user.update({ where: { id }, data: req.body });   // ← privilege escalation

Unknown fields must be rejected, not ignored, and an object must never be spread straight into an ORM write. Build the update from explicitly named fields:

ts
const { email, displayName } = parsed.data;
await db.user.update({ where: { id }, data: { email, displayName } });

This is the same defect as params.permit misuse in Rails and @ModelAttribute binding in Spring. It is consistently in the OWASP top ten and it is one line to prevent.


#Allowlist everything, and bound everything

InputRule
StringsmaxLength on every one. A text field with no cap is a memory vector
Arraysmin and max length
NumbersExplicit range, and integer-vs-float stated
EnumerationsA closed set, never free text
IdentifiersFormat-checked (uuid, prefixed opaque id)
DatesRFC 3339, plus a sane range — reject year 9999
Body sizeFramework-level limit (express.json({ limit: "100kb" }))
Content typeValidated and rejected when unexpected
UploadsType by magic bytes, not by extension or Content-Type; size capped

Never write a denylist. if (input.includes("<script>")) is bypassed by <ScRiPt>, <img onerror>, and a hundred other encodings. Enumerate what is allowed.

Sanitising by stripping characters is worse than rejecting: it produces a value that passed no check and matches nothing you specified. Reject, and say why.

Compressed request bodies need a decompressed-size cap as well — a 1 KB gzip payload can expand to gigabytes.


#Shape, then business rules

Two distinct layers, and they belong in different places:

LayerChecksWhereResponse
ShapeTypes, ranges, formats, required fieldsEdge, before business logic422 with field errors
Business rulesUniqueness, balance, state transitions, permissionsDomain service, inside the transaction409/422 with a domain code

A uniqueness check is not shape validation. Checking "is this email taken?" before inserting is a race — two concurrent requests both see "free". The database constraint is the guarantee; the pre-check is only a nicer error message. → Database/schema-design


#Validation is not encoding

Validated input is still untrusted in a different context. A name that is perfectly valid input is still dangerous when concatenated into SQL, a shell command, a file path, or HTML.

  • Parameterise SQL. → Security/sql-injection
  • Never build shell commands from input. → Security/command-injection
  • Resolve and confine file paths. → Security/path-traversal
  • Escape on output, per context. → Security/xss
  • Block private address ranges when fetching a supplied URL (SSRF).

Validation reduces the surface. Context-correct encoding is what actually prevents injection.


#Error responses

json
{ "code": "validation_failed", "message": "Validation failed", "requestId": "req_01J8Z",
  "errors": [
    { "field": "items.0.qty", "code": "out_of_range", "message": "Must be between 1 and 999." },
    { "field": "currency",    "code": "invalid_enum", "message": "Must be one of EUR, USD, GBP." }
  ] }
  • Return all failures at once, not the first. Otherwise the client fixes one field per round trip.
  • Use a path (items.0.qty) that identifies the exact field.
  • Stable machine code per error; the human message may change freely.
  • Never echo the rejected value back if it might be a credential. → Backend/error-handling

#Anti-patterns

Anti-patternWhy it failsFix
Relying on client-side validationThe client is attacker-controlledAlways validate server-side
Boolean validation, untyped valueEvery downstream layer re-checksParse into a typed value
Ignoring unknown fieldsMass assignment (role: "admin").strict() and reject
Spreading the body into an ORM writeAny column becomes settableName the fields explicitly
Denylist filteringBypassed by encoding variantsAllowlist
Sanitising by strippingProduces an unspecified valueReject with a reason
Unbounded strings and arraysMemory exhaustionmaxLength / max everywhere
No body size limitTrivial DoSFramework-level cap
No decompressed-size capZip-bomb expansionRatio and absolute limits
Upload type from extensionTrivially spoofedMagic-byte detection
Uniqueness checked before insert onlyRaces under concurrencyDatabase constraint
Business rules at the edgeDuplicated and drifts from the domainEnforce in the service
Validation treated as injection preventionWrong layerEncode per output context
First-error-only responsesOne round trip per fieldReturn all errors
Rejected values echoed backMay log credentialsRedact

#Checklist

  • Verify: Every request body, query and path parameter is parsed against a schema
  • Verify: Parsing produces a typed value used downstream
  • Verify: Unknown fields are rejected, not ignored
  • Verify: No request object is spread into a database write
  • Verify: Every string, array and number has explicit bounds
  • Verify: Enumerations are closed sets
  • Verify: Body size, decompressed size and upload size are capped
  • Verify: Upload types are detected from content, not from the filename
  • Verify: Allowlists are used throughout; no denylist filtering
  • Verify: Shape validation is at the edge; business rules are in the domain layer
  • Verify: Uniqueness and invariants are enforced by database constraints
  • Verify: Output encoding is applied per context, independently of validation
  • Verify: Client-supplied URLs are SSRF-guarded before any fetch
  • Verify: Validation errors list every failure with a field path and a stable code
  • Verify: Rejected values are not echoed when they may be sensitive