hema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sortBy: z.enum(['created_at', 'updated_at', 'status']).default('created_at'),
sortOrder: z.enum(['asc', 'desc']).default('desc'),
});
// Middleware or route handler
async function handleListRequest(req: Request, res: Response) {
const validationResult = PaginationSchema.safeParse(req.query);
if (!validationResult.success) {
return res.status(400).json({
code: 'INVALID_QUERY',
details: validationResult.error.flatten().fieldErrors,
});
}
const { page, limit, sortBy, sortOrder } = validationResult.data;
// Proceed to data layer with guaranteed safe values
}
**Architecture Rationale:**
- `z.coerce` handles string-to-number conversion safely, preventing type confusion attacks.
- `z.enum` restricts input to a known whitelist, eliminating arbitrary column injection.
- `safeParse` enables graceful error handling without throwing uncaught exceptions.
- Validation occurs at the route boundary, ensuring business logic never touches untrusted data.
### 2. Parameterized Data Access
Data access layers must never interpolate user input into query strings. Both SQL and NoSQL engines provide parameterized APIs that separate query structure from data values. ORMs abstract these APIs, but developers must explicitly use them.
**SQL Parameterization (TypeORM/Kysely):**
```typescript
import { DataSource } from 'typeorm';
import { Product } from './entities/Product';
const dataSource = new DataSource({ /* config */ });
const repo = dataSource.getRepository(Product);
async function searchProducts(keyword: string, category: string) {
return repo.createQueryBuilder('p')
.where('p.category = :cat', { cat: category })
.andWhere('p.name ILIKE :search', { search: `%${keyword}%` })
.getMany();
}
NoSQL Operator Sanitization:
MongoDB and similar engines interpret keys starting with $ as operators. Unfiltered payloads can bypass authentication or manipulate queries.
function stripOperators(input: Record<string, unknown>): Record<string, string> {
const sanitized: Record<string, string> = {};
for (const [key, value] of Object.entries(input)) {
if (key.startsWith('$')) {
throw new Error('Operator injection detected');
}
if (typeof value !== 'string') {
throw new Error('Non-string value rejected');
}
sanitized[key] = value;
}
return sanitized;
}
Architecture Rationale:
- Parameterized queries force the database driver to treat values as data, not executable syntax.
- Explicit operator stripping prevents
$gt, $ne, or $regex from altering query logic.
- Query builders should be composed programmatically, never assembled via string concatenation.
3. Cryptographic & Object Boundary Hardening
JWT verification and object manipulation require explicit configuration to prevent algorithm confusion and prototype pollution.
Strict JWT Verification:
import jwt from 'jsonwebtoken';
import { readFileSync } from 'fs';
const PUBLIC_KEY = readFileSync('./certs/public.pem', 'utf8');
function verifyToken(token: string) {
return jwt.verify(token, PUBLIC_KEY, {
algorithms: ['RS256'],
maxAge: '1h',
clockTolerance: 30,
});
}
Prototype Pollution Prevention:
Recursive object merging without schema constraints allows attackers to inject __proto__ or constructor keys, modifying global object behavior.
import { z } from 'zod';
const UserPreferencesSchema = z.object({
theme: z.enum(['system', 'light', 'dark']),
emailAlerts: z.boolean(),
dashboardLayout: z.array(z.string()).max(10),
});
function applyPreferences(current: z.infer<typeof UserPreferencesSchema>, incoming: unknown) {
const parsed = UserPreferencesSchema.safeParse(incoming);
if (!parsed.success) {
throw new Error('Invalid preference payload');
}
// Only merge whitelisted keys
return {
...current,
theme: parsed.data.theme,
emailAlerts: parsed.data.emailAlerts,
dashboardLayout: parsed.data.dashboardLayout,
};
}
Architecture Rationale:
- Explicit
algorithms array blocks none and symmetric fallback attacks.
- Schema whitelisting ensures only known keys are merged, neutralizing prototype pollution.
- Cryptographic operations should fail fast rather than silently accepting malformed tokens.
Pitfall Guide
1. The Static Type Mirage
Explanation: TypeScript interfaces and type aliases are erased during compilation. They provide zero runtime protection against malformed, malicious, or operator-injected payloads. A function typed as (data: User) => void will happily accept { __proto__: { admin: true } } at runtime.
Fix: Treat all external input as untrusted. Apply runtime schema validation at every network boundary, regardless of TypeScript types.
2. ORM Abstraction Leakage
Explanation: ORMs and ODMs do not automatically sanitize inputs. Using Raw(), string interpolation in .where(), or passing req.body directly to .find() bypasses built-in protections and enables injection attacks.
Fix: Always use parameterized placeholders (:param or ?). Validate inputs before passing them to query builders. Never trust framework auto-mapping for security-critical fields.
3. Unbounded Object Merging
Explanation: Recursive deep merge functions that iterate over Object.keys() or use for...in loops without hasOwnProperty checks allow attackers to inject __proto__ or constructor keys, modifying Object.prototype and altering runtime behavior across the entire application.
Fix: Replace generic merge utilities with schema-driven assignment. Only copy keys that exist in a predefined whitelist. Use structuredClone() for safe duplication when needed.
4. Algorithm Ambiguity in Token Verification
Explanation: Omitting the algorithms array in jwt.verify() allows attackers to forge tokens using the none algorithm or symmetric algorithms (HS256) when asymmetric keys (RS256/ES256) are expected. This enables complete authentication bypass.
Fix: Always specify algorithms: ['RS256'] (or your chosen asymmetric algorithm). Validate token expiration and issuer claims explicitly. Never rely on library defaults for security boundaries.
5. Silent Environment Fallbacks
Explanation: Applications that boot with missing or weak secrets often fall back to hardcoded defaults or generate temporary keys. This creates inconsistent security postures across environments and enables credential leakage in logs or error traces.
Fix: Validate all environment variables at startup using a schema parser. Throw a fatal error if required secrets are missing, malformed, or below minimum entropy thresholds. Integrate secret scanning into CI/CD pipelines.
6. NoSQL Operator Bypass
Explanation: JSON payloads containing MongoDB operators like $gt, $ne, or $regex can alter query logic, bypass authentication checks, or extract unauthorized records. Frameworks that automatically parse JSON bodies do not filter these operators by default.
Fix: Implement a request middleware that strips or rejects keys starting with $. Use schema validation to enforce string-only values for filter parameters. Never pass raw request bodies directly to ODM query methods.
7. Decode-Verify Conflation
Explanation: jwt.decode() parses the token payload without verifying the signature, expiration, or issuer. Using it for authentication decisions allows attackers to submit forged or expired tokens that appear valid.
Fix: Reserve jwt.decode() for debugging or logging. Always use jwt.verify() at authentication boundaries. Validate exp, iat, iss, and aud claims explicitly.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-traffic public API | Zod + Fastify | Zero-cost parsing, built-in schema validation, minimal overhead | Low (dev time) |
| Enterprise internal service | class-validator + NestJS | Decorator-driven, integrates with DI, strong typing | Medium (boilerplate) |
| Legacy codebase migration | Incremental schema wrapping | Validates boundaries without rewriting core logic | Low (phased rollout) |
| Multi-tenant SaaS | Strict JWT + RBAC schema | Prevents cross-tenant data leakage via token manipulation | Low (config) |
| NoSQL-heavy workloads | Operator stripping + schema validation | Neutralizes $gt/$regex injection vectors | Low (middleware) |
Configuration Template
// src/security/startup.ts
import { z } from 'zod';
import { readFileSync } from 'fs';
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'staging', 'production']),
JWT_PUBLIC_KEY_PATH: z.string().min(1),
DB_CONNECTION_STRING: z.string().url(),
RATE_LIMIT_WINDOW_MS: z.coerce.number().min(1000).default(60000),
RATE_LIMIT_MAX_REQUESTS: z.coerce.number().min(10).default(100),
});
export const config = EnvSchema.parse(process.env);
export const JWT_PUBLIC_KEY = readFileSync(config.JWT_PUBLIC_KEY_PATH, 'utf8');
// src/security/validators.ts
import { z } from 'zod';
export const AuthTokenSchema = z.object({
authorization: z.string().regex(/^Bearer\s+[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/),
});
export const FilterSchema = z.object({
status: z.enum(['active', 'inactive', 'pending']).optional(),
tags: z.array(z.string().max(50)).max(10).optional(),
dateRange: z.object({
start: z.coerce.date(),
end: z.coerce.date().min(z.refine((d) => d >= d.start)),
}).optional(),
});
// src/security/jwt.ts
import jwt from 'jsonwebtoken';
import { config, JWT_PUBLIC_KEY } from './startup';
export function verifySession(token: string) {
return jwt.verify(token, JWT_PUBLIC_KEY, {
algorithms: ['RS256'],
maxAge: '24h',
clockTolerance: 30,
}) as { sub: string; role: string; exp: number };
}
Quick Start Guide
- Install Dependencies: Run
npm install zod jsonwebtoken class-validator reflect-metadata to establish validation and cryptographic tooling.
- Define Startup Schema: Create an environment validation file that parses
process.env at application boot. Throw a fatal error if required variables are missing or malformed.
- Wrap Route Boundaries: Replace TypeScript interfaces for request/query parameters with Zod schemas. Apply
safeParse at the start of every handler and return structured 400 errors on failure.
- Parameterize All Queries: Audit database access layers. Replace string concatenation with parameterized placeholders. Add operator-stripping middleware for NoSQL collections.
- Enforce JWT Strictness: Update all token verification calls to include
algorithms: ['RS256'] and explicit claim validation. Store public keys outside source control and load them at startup.