er trust client-supplied data. Validation must occur at the API boundary using explicit schemas that enforce type, format, and business constraints. Zod provides runtime validation with TypeScript type inference, eliminating the gap between static types and runtime data.
import { z } from 'zod';
const UserRegistrationSchema = z.object({
email: z.string().email().max(254),
password: z.string().min(12).regex(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{12,}$/),
role: z.enum(['user', 'admin']).default('user'),
metadata: z.record(z.string(), z.unknown()).optional()
});
export function validateRegistration(payload: unknown) {
return UserRegistrationSchema.parse(payload);
}
Architecture rationale: Schema validation acts as the first defense layer. By rejecting malformed or malicious payloads before business logic execution, you prevent injection vectors, type confusion, and unexpected state mutations. Centralize schemas in a shared validation module to ensure consistency across routes, workers, and message consumers.
2. Context-Aware Output Encoding
Rendering untrusted data without context-aware encoding enables cross-site scripting (XSS) and template injection. Encoding must match the output context: HTML, JavaScript, URL, or CSS.
import { escape } from 'lodash';
function renderUserProfile(user: { name: string; bio: string }) {
const safeName = escape(user.name);
const safeBio = escape(user.bio);
return `<div class="profile"><h1>${safeName}</h1><p>${safeBio}</p></div>`;
}
Architecture rationale: Encoding is not a UI concern; it is a data integrity control. Modern frameworks (React, Vue, Svelte) auto-escape JSX/template expressions, but server-side rendering, email templates, and markdown processors require explicit handling. Maintain an encoding utility layer that abstracts context-specific escaping rules.
3. Least-Privilege Authentication & Authorization
Authentication verifies identity; authorization enforces access. Combine short-lived JWTs with role-based access control (RBAC) and resource-level permissions. Never embed sensitive claims in tokens without verification.
import jwt from 'jsonwebtoken';
import { createHash } from 'crypto';
const JWT_SECRET = process.env.JWT_SECRET!;
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';
export function generateTokens(userId: string, roles: string[]) {
const accessToken = jwt.sign({ sub: userId, roles }, JWT_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRY });
const refreshToken = jwt.sign({ sub: userId, type: 'refresh' }, JWT_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRY });
return { accessToken, refreshToken };
}
export function authorize(role: string) {
return (req: any, res: any, next: any) => {
const userRoles = req.user?.roles || [];
if (!userRoles.includes(role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
Architecture rationale: Token expiration limits blast radius. RBAC enforces separation of duties. Always verify tokens against a trusted secret store, never hardcode keys. Pair authorization with resource-level checks (e.g., ownership validation) to prevent broken object-level authorization (BOLA).
4. Secret Management & Environment Isolation
Secrets must never reside in source control, logs, or error responses. Use environment variables backed by a vault, rotate credentials on a defined schedule, and enforce strict access boundaries.
// .env.example (committed)
JWT_SECRET=
DB_PASSWORD=
API_KEY=
// Production injection via CI/CD or vault
// Never log secrets or stack traces containing them
process.on('uncaughtException', (err) => {
const sanitized = err.message.replace(/[A-Za-z0-9+/]{20,}={0,2}/g, '[REDACTED]');
console.error(`[ERROR] ${sanitized}`);
process.exit(1);
});
Architecture rationale: Secret sprawl is a primary breach vector. Centralize credential storage using HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault. Inject secrets at runtime, never bake them into images. Implement redaction middleware to prevent accidental exposure in logs or error payloads.
5. Dependency Hygiene & Supply Chain Security
Third-party packages introduce risk. Enforce lockfile integrity, audit transitive dependencies, and pin versions. Automate vulnerability scanning and block merges on critical findings.
// package.json
{
"scripts": {
"audit": "npm audit --audit-level=high",
"postinstall": "npx audit-ci --critical"
},
"dependencies": {
"zod": "^3.22.4",
"jsonwebtoken": "^9.0.2"
}
}
Architecture rationale: Dependency attacks (typosquatting, compromised maintainers, prototype pollution) bypass traditional perimeter defenses. Lockfiles guarantee reproducible builds. Automated auditing integrated into CI prevents vulnerable packages from reaching production. Maintain a dependency review policy that requires maintainer verification for new packages.
Pitfall Guide
1. Treating Client-Side Validation as Security
Client-side checks improve UX but provide zero security. Attackers bypass UI constraints using raw HTTP requests, proxy tools, or custom scripts. Always replicate validation logic on the server boundary.
2. Hardcoding Secrets or Logging Credentials
Environment variables are not secrets; they are configuration. Storing API keys, database passwords, or JWT secrets in code or logging them during debugging creates immediate exposure. Use vault-backed injection and implement log redaction.
3. Over-Privileged Service Accounts
Granting wildcard permissions (*.* or admin roles) to microservices or CI runners violates least privilege. A compromised service becomes a lateral movement vector. Scope permissions to exact resource ARNs and required actions only.
4. Ignoring Transitive Dependency Vulnerabilities
Direct dependency scanning misses nested packages. A single vulnerable utility can compromise the entire dependency tree. Use lockfile auditing, enforce npm audit or yarn audit in CI, and monitor CVE databases for indirect exposures.
5. Suppressed Linter and Security Warnings
Disabling ESLint rules, ignoring SAST findings, or marking vulnerabilities as "won't fix" without risk assessment accumulates technical debt. Every suppression requires documented justification, owner assignment, and remediation timeline.
6. Assuming CORS Preflight Guarantees Security
CORS restricts browser-based cross-origin requests but does not protect against direct API calls. Attackers bypass CORS using servers, CLI tools, or compromised clients. Enforce authentication, rate limiting, and input validation regardless of origin headers.
7. Exposing Stack Traces in Production Errors
Detailed error responses leak internal architecture, library versions, and query structures. Standardize error responses to return only user-safe messages and correlation IDs. Log full traces server-side with restricted access.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Internal microservice communication | mTLS + service mesh RBAC | Eliminates network-level trust assumptions; enforces zero-trust within cluster | Medium (infrastructure setup) |
| Public-facing REST API | Schema validation + rate limiting + JWT auth | Prevents injection, abuse, and unauthorized access at scale | Low (developer workflow integration) |
| Data pipeline / ETL job | Secret injection + least-privilege IAM + audit logging | Isolates credentials; ensures compliance and traceability | Low (configuration overhead) |
| Third-party integration | Webhook signature verification + allowlist + idempotency keys | Prevents spoofing, replay attacks, and duplicate processing | Medium (validation logic) |
| Legacy monolith migration | Strangler pattern + parallel security validation + phased auth replacement | Reduces risk during transition; maintains uptime while hardening | High (planning + parallel run) |
Configuration Template
// .eslintrc.js
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'security', 'no-unsafe'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:security/recommended',
'plugin:no-unsafe/recommended'
],
rules: {
'security/detect-object-injection': 'error',
'security/detect-non-literal-fs-filename': 'warn',
'security/detect-non-literal-regexp': 'warn',
'no-unsafe/regex': 'error',
'no-unsafe/unary': 'error'
},
overrides: [
{
files: ['**/*.test.ts'],
rules: {
'security/detect-non-literal-fs-filename': 'off'
}
}
]
}
// package.json (pre-commit hook setup)
{
"scripts": {
"prepare": "husky install",
"lint:security": "eslint . --ext .ts --ext .tsx",
"audit:deps": "npm audit --audit-level=high"
},
"devDependencies": {
"husky": "^8.0.3",
"lint-staged": "^15.2.0",
"eslint": "^8.56.0",
"eslint-plugin-security": "^1.7.1",
"eslint-plugin-no-unsafe": "^0.1.1"
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"npm run audit:deps"
]
}
}
Quick Start Guide
- Initialize validation layer: Install
zod and create a src/validation/ directory. Define schemas for all incoming payloads and export parse functions.
- Configure linting: Run
npm i -D eslint eslint-plugin-security eslint-plugin-no-unsafe husky lint-staged. Initialize husky with npx husky install and add the pre-commit hook to run lint-staged.
- Enable dependency auditing: Add
npm audit --audit-level=high to your CI pipeline. Configure the pipeline to fail on critical or high severity findings.
- Standardize error handling: Create a global error middleware that catches exceptions, sanitizes messages, logs full traces server-side, and returns
{ error: 'Internal server error', correlationId: '...' }.
- Rotate secrets: Move all credentials to your cloud provider's secret manager. Update deployment scripts to inject secrets at runtime. Verify no secrets exist in git history using
git-secrets or truffleHog.
Secure coding is not a compliance checkbox. It is a deterministic engineering practice that reduces risk, preserves velocity, and aligns development output with operational resilience. Implement the baseline, enforce it consistently, and measure the reduction in vulnerability density. The return on investment compounds with every commit.