Security — Practical
Security — Practical patterns
Section titled “Security — Practical patterns”Password hashing (Node, argon2)
Section titled “Password hashing (Node, argon2)”import argon2 from 'argon2';
const hash = await argon2.hash(password, { type: argon2.argon2id, memoryCost: 65536, timeCost: 3, parallelism: 1,});
const ok = await argon2.verify(hash, candidate);JWT verify (Node, jose)
Section titled “JWT verify (Node, jose)”import { jwtVerify, createRemoteJWKSet } from 'jose';
const JWKS = createRemoteJWKSet(new URL(`${process.env.IDP}/.well-known/jwks.json`));
export async function authenticate(req, res, next) { const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).end(); try { const { payload } = await jwtVerify(token, JWKS, { issuer: process.env.IDP, audience: 'api.example.com', algorithms: ['RS256','ES256'], }); req.user = payload; next(); } catch { res.status(401).end(); }}Auth + role middleware
Section titled “Auth + role middleware”const requireRole = (role: string) => (req, res, next) => req.user?.roles?.includes(role) ? next() : res.status(403).end();
app.delete('/admin/users/:id', authenticate, requireRole('admin'), handler);Authorization at the data layer (PG row-level security)
Section titled “Authorization at the data layer (PG row-level security)”ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY owner_only ON documentsUSING (owner_id = current_setting('app.user_id')::uuid);
-- on every connectionSET app.user_id = '...';Rate limiter middleware (express, redis)
Section titled “Rate limiter middleware (express, redis)”import rateLimit from 'express-rate-limit';import RedisStore from 'rate-limit-redis';
app.use(rateLimit({ store: new RedisStore({ sendCommand: (...a) => redis.call(...a) }), windowMs: 60_000, max: 100, standardHeaders: 'draft-7', keyGenerator: (req) => req.user?.id ?? req.ip,}));Login-specific rate limiter (per-account)
Section titled “Login-specific rate limiter (per-account)”async function recordFailedLogin(email: string) { const k = `fail:${email}`; const n = await redis.incr(k); if (n === 1) await redis.expire(k, 900); if (n >= 5) throw new Error('locked: try again in 15m');}Validation at the boundary (zod)
Section titled “Validation at the boundary (zod)”import { z } from 'zod';
const Body = z.object({ email: z.string().email(), age: z.number().int().min(13).max(120), role: z.enum(['user','admin']).default('user'),});
app.post('/users', (req, res) => { const data = Body.parse(req.body); // throws 400 via error handler ...});SSRF guard
Section titled “SSRF guard”import { URL } from 'url';import dns from 'dns/promises';
const PRIVATE = [ /^10\./, /^192\.168\./, /^172\.(1[6-9]|2\d|3[01])\./, /^127\./, /^169\.254\./, /^::1$/, /^fc00:/, /^fe80:/,];
async function safeFetch(input: string) { const u = new URL(input); if (!['http:', 'https:'].includes(u.protocol)) throw new Error('proto'); const { address } = await dns.lookup(u.hostname); if (PRIVATE.some(re => re.test(address))) throw new Error('private'); return fetch(input, { redirect: 'manual' });}Cookie configuration
Section titled “Cookie configuration”res.cookie('sid', sessionId, { httpOnly: true, secure: true, sameSite: 'lax', path: '/', maxAge: 1000 * 60 * 60 * 4, // 4h});Security headers (helmet)
Section titled “Security headers (helmet)”import helmet from 'helmet';
app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "https://cdn.example.com"], }, }, hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },}));CSRF protection (csurf alternative for non-API forms)
Section titled “CSRF protection (csurf alternative for non-API forms)”For SPA + cookies: double-submit cookie + custom header check on state-changing requests.
app.use((req, res, next) => { if (req.method === 'GET') { res.cookie('csrf', crypto.randomUUID(), { sameSite: 'lax' }); } else { if (req.cookies.csrf !== req.headers['x-csrf']) return res.status(403).end(); } next();});Secrets via Vault (sketch)
Section titled “Secrets via Vault (sketch)”import VaultClient from 'node-vault';const vault = VaultClient({ endpoint: 'https://vault:8200', token: process.env.VAULT_TOKEN });
const { data } = await vault.read('secret/data/db');const dbPassword = data.data.password;In Kubernetes, use External Secrets Operator to sync into K8s Secrets, mount as env or files.
TLS in Node
Section titled “TLS in Node”import https from 'https';import fs from 'fs';
https.createServer({ cert: fs.readFileSync('cert.pem'), key: fs.readFileSync('key.pem'), minVersion: 'TLSv1.2',}, app).listen(8443);For prod: terminate at LB / ingress; backend speaks plaintext over private network or mTLS via mesh.
Audit log
Section titled “Audit log”Record sensitive actions (auth events, role changes, data exports, privileged ops):
await audit.log({ actor: req.user.id, action: 'role.change', resource: target.id, meta: { from: prev, to: next }, ip: req.ip, ua: req.headers['user-agent'],});Keep audit log immutable / append-only; ship to separate store (S3 with Object Lock, immutable bucket).
Dependency hygiene
Section titled “Dependency hygiene”npm audit/pip-audit/govulncheckin CI.- Snyk / Dependabot for upgrades.
- Pin versions; lockfiles committed.
- SBOM generation (Syft) for compliance.
Checklist before shipping
Section titled “Checklist before shipping”- All inputs validated (zod / pydantic / joi).
- All DB calls parameterized.
- No secrets in code / env files committed.
- Auth + authz on every endpoint.
- Rate limiting on auth endpoints.
- HTTPS + HSTS.
- Cookies HttpOnly + Secure + SameSite.
- Logs scrub PII / tokens.
- Dependencies scanned, no critical CVEs.
- Container runs non-root, read-only fs where possible.