PushBackLog

Input Validation

Hard enforcement Complete by PushBackLog team
Topic: security Topic: quality Skillset: backend Skillset: frontend Technology: generic Stage: execution Stage: review

Input Validation

Status: Complete
Category: Security
Default enforcement: Hard
Author: PushBackLog team


Tags

  • Topic: security, quality
  • Skillset: backend, frontend
  • Technology: generic
  • Stage: execution, review

Summary

All input entering a system boundary must be validated against an expected schema before being processed or stored. Validation must occur server-side regardless of any client-side validation present; trusting client input without verification is one of the most common and exploitable security mistakes.


Rationale

The threat model

Any data that arrives at your server from outside your system — HTTP request bodies, query parameters, path parameters, headers, uploaded files, webhook payloads — is attacker-controlled. An attacker can send any payload they choose, bypassing forms, mobile apps, and browser-based validation entirely. Treating unvalidated external data as trustworthy is the root cause of OWASP A05 (Injection) and contributes directly to A01 (Broken Access Control).

Client-side validation is UX, not security

Frontend validation (form field constraints, JavaScript validation) is valuable for user experience — it gives immediate feedback without a round-trip. It provides zero security. Attackers do not use your form; they use curl, Postman, or custom scripts. Every validation on the frontend must also exist on the backend.

Validation is the first line of defence in depth

Input validation is not a complete security solution — parameterised queries, output encoding, and authorisation checks are all independently necessary. But it is the first layer. Data that cannot pass schema validation cannot reach the layers where it might do damage.


Guidance

What to validate at each boundary

Input sourceValidate
HTTP request bodySchema (required fields, types, formats, bounds)
Query parametersPresence, type, permitted values, length
Path parameters (:id)Format (UUID, numeric), existence, then ownership
File uploadsMIME type (from magic bytes, not extension), size limit, filename
Webhook payloadsSchema + HMAC signature verification
JWT / session tokensSignature, expiry, issuer, audience

Validation vs sanitisation

OperationPurposeWhen to use
ValidationReject input that doesn’t meet the expected schemaAlways, at every boundary
SanitisationTransform input to remove dangerous contentFor output encoding (HTML, SQL); not a substitute for validation
AllowlistingAccept only known-good valuesPreferred for constrained inputs (enums, IDs, codes)
DenylistingReject known-bad patternsFragile — use only when allowlisting is not possible

Prefer allowlisting. If a field should be a UUID, validate that it is a UUID — don’t search it for SQL keywords.

Schema validation libraries

StackLibrary
TypeScript / NodeZod, Joi, class-validator
PythonPydantic, voluptuous
Java / KotlinJakarta Bean Validation, Valiktor
Gogo-playground/validator
Rubydry-validation

Validate early, fail loudly

Validation should happen as early as possible in the request lifecycle — ideally in middleware or a dedicated validation layer before any business logic executes. Validation failures should return a clear, safe error response (422 Unprocessable Entity) with no internal detail.


Examples

API endpoint with schema validation (TypeScript + Zod)

import { z } from 'zod';

const CreateOrderSchema = z.object({
  userId: z.string().uuid(),
  items: z.array(z.object({
    productId: z.string().uuid(),
    quantity: z.number().int().positive().max(100),
  })).min(1).max(50),
  discountCode: z.string().max(20).regex(/^[A-Z0-9-]+$/).optional(),
});

app.post('/api/orders', (req, res) => {
  const result = CreateOrderSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(422).json({ errors: result.error.flatten() });
  }
  // result.data is now type-safe and validated
  return orderService.create(result.data);
});

Unknown fields are stripped. Invalid types, out-of-bound values, and malformed UUIDs are all rejected before they reach business logic.

File upload validation

import fileType from 'file-type';

async function validateUpload(file: Buffer, allowedTypes: string[]): Promise<void> {
  const detected = await fileType.fromBuffer(file);
  if (!detected || !allowedTypes.includes(detected.mime)) {
    throw new ValidationError('Invalid file type');
  }
  if (file.length > 5 * 1024 * 1024) {
    throw new ValidationError('File exceeds the 5MB limit');
  }
}

Note: MIME type is read from file bytes (magic bytes), not from the extension or Content-Type header — both of which are attacker-controlled.


Anti-patterns

1. Client-only validation

“We validate on the form.” Server-side validation is not optional. No client-side validation provides any server-side protection.

2. Trusting user-supplied IDs for ownership

const { orderId, userId } = req.body; // userId supplied by the client
order = await getOrder(orderId, userId); // Attacker sets userId to someone else's

User identity must come from the authenticated session/token, never from request body.

3. Using unsanitised input in queries or commands

Even with validation in place, any input used in a database query must use parameterisation. Validation reduces attack surface; parameterisation prevents injection.

4. Catching exceptions from downstream instead of validating upfront

Relying on the database to reject invalid data via a constraint violation is not input validation — it’s error recovery. Validation must happen at the boundary, before the database is consulted.

5. Trusting Content-Type headers for file uploads

An attacker can upload a PHP script with Content-Type: image/jpeg. Detect file type from magic bytes only.



Part of the PushBackLog Best Practices Library. Suggest improvements →