Secure Backend Scaffold Defaults and Hardening Patterns
Research conducted: 2026-02-10
Executive Summary
- JWT verification must use cryptographic libraries like
josewith explicit algorithm whitelisting -- never decode tokens with base64 alone or allow algorithm negotiation from untrusted input. Thejoselibrary provides constant-time signature verification, JWK/JWKS support, and protection against algorithm confusion attacks thatjsonwebtokenhistorically lacked. - Session management via httpOnly, Secure, SameSite cookies using
@fastify/secure-sessioneliminates an entire class of XSS-based token theft -- server-side encrypted sessions with sodium-native provide defense-in-depth that localStorage-based JWT storage cannot match. - Rate limiting, CSRF protection, and Content Security Policy must be scaffold defaults, not afterthoughts --
@fastify/rate-limit,@fastify/csrf-protection, and@fastify/helmetshould be registered at application bootstrap with secure presets, overridden per-route only when justified. - Input sanitization beyond schema validation is critical -- Fastify's JSON Schema validation handles type checking but does not prevent stored XSS, prototype pollution, or semantic injection; dedicated sanitization layers and parameterized queries via ORMs are required.
- OWASP Top 10 (2021) maps directly to Fastify plugin defaults -- a well-configured scaffold with helmet, CORS restrictions, rate limiting, secure sessions, CSRF tokens, parameterized ORM queries, and dependency auditing mitigates 9 of 10 categories out of the box.
Background & Context
Modern backend scaffolds and code generators (such as create-fastify, Yeoman generators, or custom CLI tools) determine the security posture of every application built from them. When a scaffold ships with insecure defaults -- such as permissive CORS (origin: *), no rate limiting, or JWT tokens stored in localStorage -- every downstream project inherits those vulnerabilities. The concept of "secure by default" means that a freshly generated project, before any developer customization, should be hardened against the OWASP Top 10 and common attack vectors.
The Fastify ecosystem is particularly well-suited for secure-by-default scaffolding because of its plugin architecture. Security concerns are encapsulated in first-party plugins (@fastify/helmet, @fastify/cors, @fastify/rate-limit, @fastify/csrf-protection, @fastify/secure-session) that can be registered declaratively. Unlike Express middleware ordering pitfalls, Fastify's encapsulation model ensures security plugins apply to the correct scope.
This research focuses on the Fastify/Node.js stack and synthesizes patterns from the OWASP Cheat Sheet Series, Fastify official documentation, the jose library documentation, and community security hardening guides. The goal is to provide actionable patterns that a code generator can embed as defaults, reducing the "time to secure" for new backend projects from days to zero.
Key Findings
1. JWT Verification with jose vs Insecure Base64 Decoding
The jose library (by Filip Skokan, also author of oidc-provider) is the recommended JWT library for Node.js security-critical applications. It provides:
Why jose over jsonwebtoken:
- Algorithm whitelisting:
joserequires explicit algorithm specification viajwtVerify(), preventing algorithm confusion attacks where an attacker switches RS256 to HS256 and signs with the public key. - Constant-time comparison: Signature verification uses timing-safe comparison, preventing timing side-channel attacks.
- JWK/JWKS support: Native support for JSON Web Key Sets, enabling key rotation without code changes.
- No
nonealgorithm:josedoes not accept thenonealgorithm by default, unlike somejsonwebtokenconfigurations. - ESM-first, zero dependencies: Smaller attack surface compared to
jsonwebtoken's dependency tree.
Critical anti-pattern -- base64 decoding without verification:
// DANGEROUS: Never do this
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());
// This trusts the token without verifying the signature
// SECURE: Always verify with jose
import { jwtVerify } from 'jose';
const { payload } = await jwtVerify(token, publicKey, {
algorithms: ['RS256'], // Explicit algorithm whitelist
issuer: 'https://auth.example.com', // Validate issuer
audience: 'my-api', // Validate audience
clockTolerance: 30, // 30-second clock skew tolerance
});
Scaffold default pattern:
// config/auth.js - Scaffold default
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(new URL(process.env.JWKS_URI));
export async function verifyToken(token) {
const { payload, protectedHeader } = await jwtVerify(token, JWKS, {
algorithms: ['RS256', 'ES256'],
issuer: process.env.JWT_ISSUER,
audience: process.env.JWT_AUDIENCE,
maxTokenAge: '1h',
});
return payload;
}
Key security properties:
algorithmsmust be an explicit array, never inferred from the token header.issuerandaudiencevalidation prevents token reuse across services.maxTokenAgeenforces time-based expiry server-side, independent of theexpclaim.createRemoteJWKSetcaches JWKS responses and handles key rotation automatically.
2. httpOnly Cookie Session Management
@fastify/secure-session over @fastify/session:
@fastify/secure-session uses sodium-native (libsodium bindings) for encrypted, tamper-proof cookies. Unlike @fastify/session which relies on server-side storage (Redis, etc.), @fastify/secure-session is stateless -- the encrypted session data lives entirely in the cookie, eliminating the need for a session store.
Scaffold default configuration:
import fastifySecureSession from '@fastify/secure-session';
import { readFileSync } from 'fs';
fastify.register(fastifySecureSession, {
// Generated via: npx @fastify/secure-session > secret-key
key: readFileSync('secret-key'),
// OR use environment variable
// key: Buffer.from(process.env.SESSION_SECRET, 'hex'),
cookie: {
path: '/',
httpOnly: true, // Not accessible via document.cookie
secure: true, // Only sent over HTTPS
sameSite: 'lax', // CSRF protection for top-level navigation
maxAge: 3600, // 1 hour in seconds
domain: process.env.COOKIE_DOMAIN,
},
});
Why these defaults matter:
httpOnly: true-- Prevents JavaScript from reading the session cookie, mitigating XSS-based session theft entirely.secure: true-- Ensures cookies are only sent over HTTPS, preventing MITM interception on downgraded connections.sameSite: 'lax'-- Prevents cross-origin form submissions from sending the cookie, providing baseline CSRF protection. Use'strict'for highest security (breaks OAuth redirects) or'none'only withsecure: truefor legitimate cross-origin needs.maxAge: 3600-- Sessions expire after 1 hour of inactivity, limiting the window for stolen cookies.
Session data patterns:
// Setting session data after login
fastify.post('/login', async (request, reply) => {
const user = await authenticateUser(request.body);
request.session.set('userId', user.id);
request.session.set('role', user.role);
request.session.set('loginAt', Date.now());
return { success: true };
});
// Reading session data in protected routes
fastify.get('/profile', async (request, reply) => {
const userId = request.session.get('userId');
if (!userId) {
return reply.code(401).send({ error: 'Unauthorized' });
}
// ...
});
// Destroying session on logout
fastify.post('/logout', async (request, reply) => {
request.session.delete();
return { success: true };
});
3. CSRF Protection Patterns
@fastify/csrf-protection with double-submit cookie pattern:
import fastifyCsrf from '@fastify/csrf-protection';
fastify.register(fastifyCsrf, {
sessionPlugin: '@fastify/secure-session',
cookieOpts: {
httpOnly: true,
sameSite: 'strict',
secure: true,
signed: true,
path: '/',
},
getToken: (request) => {
return request.headers['x-csrf-token'] ||
request.body?._csrf;
},
});
Protection strategies for different architectures:
| Architecture | Strategy | Implementation |
|---|---|---|
| Server-rendered (HTML forms) | Synchronizer Token Pattern | Embed csrfToken() in hidden form field |
| SPA + Same-origin API | Double Submit Cookie | Send token in x-csrf-token header |
| SPA + Cross-origin API | SameSite cookies + Custom header | Rely on SameSite=Strict + check Origin header |
| API-only (no cookies) | Not needed | Stateless JWT auth does not need CSRF protection |
Per-route CSRF configuration:
// Exempt webhook endpoints from CSRF
fastify.post('/webhooks/stripe', {
config: { csrf: false },
preHandler: verifyStripeSignature,
}, webhookHandler);
// Enforce CSRF on all other mutation routes
fastify.addHook('onRequest', async (request, reply) => {
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(request.method)) {
await fastify.csrfProtection(request, reply);
}
});
4. Rate Limiting Strategies
@fastify/rate-limit configuration patterns:
import fastifyRateLimit from '@fastify/rate-limit';
// Global rate limit
fastify.register(fastifyRateLimit, {
global: true,
max: 100, // 100 requests
timeWindow: '1 minute', // per minute
ban: 3, // Ban after 3 limit hits
cache: 10000, // Cache size for IP tracking
allowList: ['127.0.0.1'], // Exempt localhost in development
keyGenerator: (request) => {
// Use authenticated user ID if available, otherwise IP
return request.user?.id || request.ip;
},
errorResponseBuilder: (request, context) => ({
statusCode: 429,
error: 'Too Many Requests',
message: `Rate limit exceeded. Retry in ${context.after}`,
retryAfter: context.after,
}),
addHeadersOnExceeding: {
'x-ratelimit-limit': true,
'x-ratelimit-remaining': true,
'x-ratelimit-reset': true,
},
addHeaders: {
'x-ratelimit-limit': true,
'x-ratelimit-remaining': true,
'x-ratelimit-reset': true,
'retry-after': true,
},
});
Per-route rate limits (tiered strategy):
// Authentication endpoints: strict limits
fastify.post('/auth/login', {
config: {
rateLimit: {
max: 5,
timeWindow: '15 minutes',
keyGenerator: (req) => req.ip, // Always by IP for auth
},
},
}, loginHandler);
// Password reset: very strict
fastify.post('/auth/reset-password', {
config: {
rateLimit: {
max: 3,
timeWindow: '1 hour',
},
},
}, resetHandler);
// API endpoints: per-user limits
fastify.get('/api/data', {
config: {
rateLimit: {
max: 1000,
timeWindow: '1 hour',
keyGenerator: (req) => req.user.id,
},
},
}, dataHandler);
// Public read endpoints: generous limits
fastify.get('/api/public', {
config: {
rateLimit: {
max: 200,
timeWindow: '1 minute',
},
},
}, publicHandler);
Sliding window vs fixed window:
@fastify/rate-limit uses a fixed window by default. For sliding window behavior with Redis:
import Redis from 'ioredis';
fastify.register(fastifyRateLimit, {
global: true,
max: 100,
timeWindow: '1 minute',
redis: new Redis({
host: process.env.REDIS_HOST,
port: 6379,
enableOfflineQueue: false,
}),
// Redis store provides distributed rate limiting
// and approximate sliding window behavior
});
Rate limiting tiers scaffold default:
| Endpoint Category | Max Requests | Time Window | Key |
|---|---|---|---|
| Login/Register | 5 | 15 min | IP |
| Password Reset | 3 | 1 hour | IP |
| Email Verification | 5 | 1 hour | IP |
| API (authenticated) | 1000 | 1 hour | User ID |
| API (unauthenticated) | 100 | 1 min | IP |
| File Upload | 10 | 1 hour | User ID |
| Webhook receive | Unlimited | - | Signature-verified |
5. Content Security Policy and Security Headers
@fastify/helmet configuration:
import fastifyHelmet from '@fastify/helmet';
fastify.register(fastifyHelmet, {
// CSP configuration
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // No 'unsafe-inline' or 'unsafe-eval'
styleSrc: ["'self'"],
imgSrc: ["'self'", 'data:'],
fontSrc: ["'self'"],
connectSrc: ["'self'"],
frameSrc: ["'none'"], // Prevent framing entirely
objectSrc: ["'none'"], // No plugins (Flash, Java)
baseUri: ["'self'"], // Prevent base tag hijacking
formAction: ["'self'"], // Restrict form destinations
frameAncestors: ["'none'"], // Prevent clickjacking
upgradeInsecureRequests: [], // Auto-upgrade HTTP to HTTPS
},
},
// Additional headers
crossOriginEmbedderPolicy: true,
crossOriginOpenerPolicy: { policy: 'same-origin' },
crossOriginResourcePolicy: { policy: 'same-origin' },
dnsPrefetchControl: { allow: false },
frameguard: { action: 'deny' },
hsts: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true,
},
ieNoOpen: true,
noSniff: true, // X-Content-Type-Options: nosniff
permittedCrossDomainPolicies: { permittedPolicies: 'none' },
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
xssFilter: true,
});
API-only vs full-stack CSP differences:
For API-only backends (JSON responses), CSP is less critical but still recommended:
// API-only: simplified helmet
fastify.register(fastifyHelmet, {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'none'"], // API serves no HTML
frameAncestors: ["'none'"],
},
},
crossOriginResourcePolicy: { policy: 'same-site' },
});
6. CORS Configuration
Secure CORS defaults (never use origin: true or origin: '*' in production):
import fastifyCors from '@fastify/cors';
fastify.register(fastifyCors, {
origin: (origin, callback) => {
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
// Allow requests with no origin (mobile apps, curl, server-to-server)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
return callback(new Error('CORS: Origin not allowed'), false);
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'],
credentials: true, // Required for cookie-based auth
maxAge: 86400, // Cache preflight for 24 hours
exposedHeaders: ['X-RateLimit-Limit', 'X-RateLimit-Remaining'],
preflight: true,
strictPreflight: true, // Return 400 for invalid preflight
});
Environment-based CORS scaffold:
const corsConfig = {
development: {
origin: ['http://localhost:3000', 'http://localhost:5173'],
credentials: true,
},
staging: {
origin: ['https://staging.example.com'],
credentials: true,
},
production: {
origin: ['https://example.com', 'https://www.example.com'],
credentials: true,
maxAge: 86400,
},
};
7. Input Sanitization Beyond Validation
Fastify's JSON Schema validation ensures types, formats, and constraints, but it does not sanitize content. Additional layers are needed:
Prototype pollution prevention:
// Fastify 4.x+ has built-in prototype pollution protection
// via secure-json-parse, but verify configuration:
fastify.register(import('@fastify/sensible'));
// Additionally, use Object.create(null) for lookup objects:
const handlers = Object.create(null);
handlers['action'] = myHandler;
HTML/XSS sanitization for stored content:
import DOMPurify from 'isomorphic-dompurify';
function sanitizeUserInput(input) {
if (typeof input === 'string') {
// Strip all HTML tags for plain text fields
return DOMPurify.sanitize(input, { ALLOWED_TAGS: [] });
}
return input;
}
// For rich text fields (markdown, HTML editors):
function sanitizeRichText(html) {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'br', 'h1', 'h2', 'h3', 'code', 'pre'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
ALLOW_DATA_ATTR: false,
});
}
Path traversal prevention:
import path from 'path';
function safePath(userInput, baseDir) {
const resolved = path.resolve(baseDir, userInput);
if (!resolved.startsWith(path.resolve(baseDir))) {
throw new Error('Path traversal detected');
}
return resolved;
}
Fastify schema + sanitization plugin pattern:
// Pre-validation hook for sanitization
fastify.addHook('preValidation', async (request) => {
if (request.body && typeof request.body === 'object') {
request.body = deepSanitize(request.body);
}
});
function deepSanitize(obj) {
const result = {};
for (const [key, value] of Object.entries(obj)) {
// Block prototype pollution keys
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue;
}
if (typeof value === 'string') {
result[key] = DOMPurify.sanitize(value, { ALLOWED_TAGS: [] });
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
result[key] = deepSanitize(value);
} else if (Array.isArray(value)) {
result[key] = value.map(item =>
typeof item === 'string'
? DOMPurify.sanitize(item, { ALLOWED_TAGS: [] })
: typeof item === 'object' && item !== null
? deepSanitize(item)
: item
);
} else {
result[key] = value;
}
}
return result;
}
8. SQL Injection Prevention with ORMs
Prisma ORM (recommended for Fastify scaffolds):
Prisma uses parameterized queries by default, making SQL injection via the standard API effectively impossible:
// SAFE: Prisma parameterizes automatically
const user = await prisma.user.findUnique({
where: { email: userInput }, // userInput is parameterized
});
// SAFE: Prisma's query builder handles escaping
const users = await prisma.user.findMany({
where: {
name: { contains: searchTerm }, // Parameterized LIKE query
},
});
Dangerous: Raw queries bypass ORM protection:
// DANGEROUS: String interpolation in raw queries
const result = await prisma.$queryRaw`SELECT * FROM users WHERE name = '${userInput}'`;
// SAFE: Tagged template literal with Prisma
const result = await prisma.$queryRaw`SELECT * FROM users WHERE name = ${userInput}`;
// The tagged template auto-parameterizes
// SAFE: Explicit parameterization with Prisma.sql
import { Prisma } from '@prisma/client';
const result = await prisma.$queryRaw(
Prisma.sql`SELECT * FROM users WHERE name = ${Prisma.raw("$1")}`,
userInput
);
Drizzle ORM patterns:
import { eq, like, sql } from 'drizzle-orm';
// SAFE: Query builder parameterizes
const user = await db.select().from(users).where(eq(users.email, userInput));
// SAFE: Like queries
const results = await db.select().from(users).where(like(users.name, `%${userInput}%`));
// DANGEROUS: sql.raw() bypasses parameterization
// const bad = await db.execute(sql.raw(`SELECT * FROM users WHERE name = '${userInput}'`));
// SAFE: sql`` tagged template with placeholder
const safe = await db.execute(sql`SELECT * FROM users WHERE name = ${userInput}`);
Scaffold rule: Never expose $queryRaw with string concatenation. Lint rules should flag raw SQL string interpolation.
9. Secure Dependency Defaults
Package.json scaffold defaults:
{
"scripts": {
"audit": "npm audit --audit-level=high",
"audit:fix": "npm audit fix",
"preinstall": "npx npm-force-resolutions",
"prepare": "husky install"
},
"overrides": {},
"engines": {
"node": ">=20.0.0"
}
}
Recommended security tooling in scaffold:
| Tool | Purpose | Integration |
|---|---|---|
npm audit | Dependency vulnerability scanning | CI/CD pipeline, pre-commit |
socket.dev | Supply chain attack detection | GitHub App, CI check |
snyk | Deep vulnerability analysis | CLI, CI/CD, IDE |
lockfile-lint | Lockfile integrity verification | Pre-commit hook |
better-npm-audit | Enhanced npm audit with allowlisting | CI/CD |
husky + lint-staged | Pre-commit security checks | Local development |
.npmrc security defaults:
# Enforce exact versions
save-exact=true
# Require lockfile for installs
package-lock=true
# Audit on install
audit=true
# Strict SSL
strict-ssl=true
# Disable lifecycle scripts from dependencies
ignore-scripts=true
Node.js runtime security:
# Production startup with security flags
node --disable-proto=delete \
--disallow-code-generation-from-strings \
--experimental-permission \
--allow-fs-read=/app \
--allow-fs-write=/app/uploads \
server.js
10. OWASP Top 10 (2021) Mitigation in Scaffolds
| # | OWASP Category | Fastify Mitigation | Plugin/Pattern |
|---|---|---|---|
| A01 | Broken Access Control | Role-based route guards, JWT audience validation | jose + custom decorators |
| A02 | Cryptographic Failures | TLS enforcement, secure session encryption, bcrypt/argon2 | @fastify/secure-session, HSTS via helmet |
| A03 | Injection | Parameterized ORM queries, input sanitization, JSON Schema validation | Prisma/Drizzle, Fastify schema validation |
| A04 | Insecure Design | Threat modeling in scaffold docs, rate limiting by default | @fastify/rate-limit, scaffold documentation |
| A05 | Security Misconfiguration | Secure defaults for CORS, CSP, headers; no debug in prod | @fastify/helmet, @fastify/cors, env-based config |
| A06 | Vulnerable Components | Automated dependency scanning, lockfile verification | npm audit, socket.dev, lockfile-lint |
| A07 | Auth Failures | Secure session cookies, MFA support, brute-force protection | @fastify/secure-session, rate limiting on auth routes |
| A08 | Software/Data Integrity | SBOM generation, subresource integrity, signed commits | CI pipeline, @fastify/helmet SRI |
| A09 | Logging Failures | Structured logging with pino, audit trail hooks | pino (Fastify built-in), custom audit hooks |
| A10 | SSRF | URL validation, allowlisted external services, DNS rebinding protection | Custom fetch wrappers, private IP blocking |
11. Security-First Code Generation Patterns
Scaffold structure for security-first Fastify app:
src/
plugins/
security.js # Registers all security plugins
authentication.js # JWT verification, session management
authorization.js # RBAC decorators
hooks/
sanitize.js # Pre-validation sanitization
audit-log.js # Request/response audit logging
config/
cors.js # Environment-based CORS config
rate-limit.js # Tiered rate limit config
csp.js # Content Security Policy directives
routes/
auth/ # Auth routes with strict rate limits
api/ # API routes with standard limits
utils/
safe-path.js # Path traversal prevention
safe-redirect.js # Open redirect prevention
crypto.js # Hashing, token generation utilities
Security plugin registration order:
// src/plugins/security.js
import fp from 'fastify-plugin';
export default fp(async function securityPlugins(fastify) {
// 1. Helmet (security headers) -- first, applies to all responses
await fastify.register(import('@fastify/helmet'), helmetConfig);
// 2. CORS -- before route handling
await fastify.register(import('@fastify/cors'), corsConfig);
// 3. Rate limiting -- before authentication
await fastify.register(import('@fastify/rate-limit'), rateLimitConfig);
// 4. Secure session -- before CSRF (CSRF depends on session)
await fastify.register(import('@fastify/secure-session'), sessionConfig);
// 5. CSRF protection -- after session plugin
await fastify.register(import('@fastify/csrf-protection'), csrfConfig);
// 6. Sensible -- secure error handling
await fastify.register(import('@fastify/sensible'));
}, { name: 'security-plugins' });
Safe redirect utility:
// Prevent open redirect vulnerabilities
function safeRedirect(url, allowedHosts) {
try {
const parsed = new URL(url);
if (allowedHosts.includes(parsed.hostname)) {
return url;
}
} catch {
// Relative URLs are safe
if (url.startsWith('/') && !url.startsWith('//')) {
return url;
}
}
return '/'; // Default to home
}
Error handling that does not leak internals:
fastify.setErrorHandler(async (error, request, reply) => {
request.log.error(error); // Log full error server-side
// Never expose stack traces or internal details in production
if (process.env.NODE_ENV === 'production') {
const statusCode = error.statusCode || 500;
return reply.code(statusCode).send({
statusCode,
error: statusCode >= 500 ? 'Internal Server Error' : error.message,
message: statusCode >= 500
? 'An unexpected error occurred'
: error.message,
});
}
// In development, include more detail
return reply.code(error.statusCode || 500).send({
statusCode: error.statusCode || 500,
error: error.message,
stack: error.stack,
});
});
12. @fastify/helmet Deep Dive
Helmet sets 15+ HTTP security headers. Key configurations for scaffolds:
Strict Transport Security (HSTS):
maxAge: 31536000(1 year) -- tells browsers to only use HTTPSincludeSubDomains: true-- applies to all subdomainspreload: true-- allows submission to HSTS preload lists
X-Content-Type-Options:
noSniff: true-- prevents MIME-type sniffing attacks where browsers execute files as scripts
Referrer-Policy:
strict-origin-when-cross-origin-- sends full URL on same-origin, only origin on cross-origin, nothing on downgrade
Permissions-Policy (formerly Feature-Policy):
// Additional header not covered by helmet by default
fastify.addHook('onSend', async (request, reply) => {
reply.header('Permissions-Policy',
'camera=(), microphone=(), geolocation=(), payment=(self)'
);
});
13. @fastify/secure-session Deep Dive
Key generation and rotation:
# Generate encryption key
npx @fastify/secure-session > secret-key
# For key rotation, generate a new key and use both:
npx @fastify/secure-session > secret-key-new
import { readFileSync } from 'fs';
fastify.register(fastifySecureSession, {
// Key rotation: first key encrypts, all keys decrypt
key: [
readFileSync('secret-key-new'), // Current key (encrypts)
readFileSync('secret-key'), // Old key (still decrypts)
],
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 3600,
},
});
Session security patterns:
- Regenerate session ID after authentication state changes (login, privilege escalation)
- Set absolute session timeouts (not just idle timeouts)
- Store minimal data in sessions (user ID, role -- not full user objects)
- Use session.delete() on logout, not just clearing individual fields
Recent Developments (2024-2026)
Node.js Permission Model (v20+): Node.js 20 introduced an experimental permission model (--experimental-permission) that restricts filesystem, network, and child process access. Scaffolds can now ship with minimal permission sets, reducing blast radius of RCE vulnerabilities.
Fastify v5 (2024-2025): Fastify 5 introduced tighter TypeScript types, improved encapsulation boundaries, and deprecated several insecure patterns. The plugin registration system now enforces stricter dependency declarations.
jose v5+: The jose library continued to evolve with improved Ed25519/Ed448 support, better error messages for algorithm mismatches, and runtime-agnostic design (works in Node.js, Deno, Bun, and browsers).
OWASP Top 10 for LLM Applications (2025): A new OWASP list specifically targeting AI/LLM-integrated backends was published, highlighting prompt injection, insecure output handling, and training data poisoning -- relevant for scaffolds generating AI-powered backends.
Supply Chain Security Hardening: The npm ecosystem adopted stricter provenance attestations (npm provenance), SLSA framework compliance, and Sigstore signing. Modern scaffolds should generate package.json with "publishConfig": { "provenance": true } for libraries.
SameSite cookie default change: Browsers now default cookies to SameSite=Lax when no attribute is specified, providing baseline CSRF protection. However, explicit configuration in scaffolds is still required for defense-in-depth.
Content Security Policy Level 3: CSP Level 3 introduced strict-dynamic and nonce-based policies, reducing the need for complex allowlists. Scaffolds can now generate per-request nonces for inline scripts that need to be allowed.
Best Practices & Recommendations
-
Register security plugins at the top level, not per-route: Helmet, CORS, rate limiting, and session management should be the first plugins registered. Per-route overrides should loosen restrictions (never tighten what should be default).
-
Use environment variables for all security-sensitive configuration: Origins, JWT issuers, session secrets, rate limit thresholds -- none of these should be hardcoded. Scaffolds should generate
.env.examplewith secure placeholder values and validation on startup. -
Default to deny, explicitly allow: CORS origins should be an explicit allowlist. CSP should start with
default-src: 'none'and add what is needed. Routes should require authentication by default, with public routes explicitly marked. -
Validate on input, sanitize on output: Use Fastify JSON Schema for structural validation on input. Apply context-specific output encoding (HTML entity encoding for HTML contexts, parameterized queries for SQL, URL encoding for URLs) on output.
-
Ship with
npm auditin CI and pre-commit hooks: Every scaffold should include a CI step that fails on high-severity vulnerabilities and a pre-commit hook that warns on new advisories. -
Never store secrets in code or environment variables alone: For production, integrate with a secrets manager (Vault, AWS Secrets Manager, Doppler). Scaffolds should document this requirement and provide adapter patterns.
-
Use structured logging with PII redaction: Fastify's built-in pino logger should be configured with serializers that redact sensitive fields (passwords, tokens, credit card numbers) from logs.
-
Generate security documentation with the scaffold: Every generated project should include a
SECURITY.mddescribing the security architecture, threat model, and responsible disclosure process. -
Include a security test suite in the scaffold: Generate tests that verify security headers are present, rate limits work, CSRF tokens are required, authentication is enforced, and error responses do not leak internals.
-
Pin dependency versions and verify lockfile integrity: Use
save-exact=truein.npmrc, commitpackage-lock.json, and runlockfile-lintin CI to detect lockfile manipulation.
Comparisons
JWT Libraries for Node.js
| Aspect | jose | jsonwebtoken | fast-jwt |
|---|---|---|---|
| Algorithm whitelisting | Required by API | Optional (defaults to HS256) | Optional |
| Algorithm confusion protection | Built-in | Requires careful configuration | Partial |
| JWK/JWKS support | Native | Via jwks-rsa addon | Partial |
none algorithm | Rejected by default | Accepted if no secret | Rejected by default |
| Timing-safe comparison | Yes | Depends on Node.js crypto | Yes |
| Dependencies | Zero | 3 (incl. jws, lodash.includes) | 2 |
| ESM support | Native | CJS only (needs wrapper) | Native |
| Maintenance | Active (single maintainer, OIDF-adjacent) | Sporadic | Active |
| Performance | Good | Good | Fastest (claims 3-5x) |
| Security track record | No known CVEs | Multiple CVEs (incl. algorithm confusion) | No known CVEs |
| Recommendation | Best for security-critical apps | Legacy, avoid for new projects | Good for performance-critical, verify security config |
Session Management Approaches
| Aspect | @fastify/secure-session (encrypted cookie) | @fastify/session + Redis | Stateless JWT |
|---|---|---|---|
| State storage | Cookie (client-side, encrypted) | Server-side (Redis) | Token (client-side, signed) |
| Scalability | Excellent (no server state) | Good (Redis dependency) | Excellent (no server state) |
| Session revocation | Not instant (cookie expiry) | Instant (delete from Redis) | Requires deny-list |
| Data size limit | ~4KB (cookie limit) | Unlimited | ~8KB (header limit) |
| XSS resilience | Strong (httpOnly) | Strong (httpOnly) | Weak if in localStorage |
| CSRF needed | Yes (cookie-based) | Yes (cookie-based) | No (if in Authorization header) |
| Recommendation | Best for most Fastify apps | Best for large session data or instant revocation needs | Best for stateless microservices with short-lived tokens |
Rate Limiting Strategies
| Strategy | Fixed Window | Sliding Window | Token Bucket | Leaky Bucket |
|---|---|---|---|---|
| Complexity | Simple | Moderate | Moderate | Moderate |
| Burst handling | Allows burst at window boundary | Smooth distribution | Allows controlled bursts | Strict rate enforcement |
| Memory usage | Low | Moderate | Low | Low |
@fastify/rate-limit support | Default | Via Redis (approximate) | Not built-in | Not built-in |
| Fairness | Window boundary unfairness | Fair | Fair | Fair |
| Recommendation | Default for most routes | For auth/payment routes | External gateway (Kong, etc.) | External gateway |
Open Questions
- How should scaffolds handle security in serverless environments (Vercel, AWS Lambda) where plugin registration order and cold starts introduce different threat models?
- What is the optimal strategy for key rotation in
@fastify/secure-sessionacross horizontally scaled instances that share no filesystem? - How should scaffolds integrate with OpenID Connect providers while maintaining secure-by-default token handling (PKCE, token binding)?
- What patterns should scaffolds adopt for the emerging OWASP Top 10 for LLM Applications, particularly around prompt injection prevention in AI-powered backends?
- How should CSP nonce generation work in Fastify when responses are cached at the CDN layer (nonces must be unique per response)?
- What is the recommended approach for mutual TLS (mTLS) in Fastify for zero-trust architectures?
- How should scaffolds handle security policy for WebSocket connections, which bypass some HTTP-based protections (CORS, CSP)?
Sources
- OWASP Node.js Security Cheat Sheet - Comprehensive Node.js security guidelines covering input validation, authentication, session management, error handling, and server security hardening.
- OWASP Top 10 (2021) - The definitive ranking of web application security risks with mitigation strategies for each category.
- OWASP JSON Web Token Cheat Sheet - JWT security best practices including algorithm validation, token storage, and expiry handling (language-agnostic patterns).
- OWASP Session Management Cheat Sheet - Cookie security attributes, session lifecycle management, and defense against session fixation/hijacking.
- OWASP Cross-Site Request Forgery Prevention Cheat Sheet - CSRF mitigation patterns including synchronizer token, double-submit cookie, and SameSite cookie approaches.
- OWASP Content Security Policy Cheat Sheet - CSP directive configuration, nonce-based policies, and deployment strategies.
- OWASP SQL Injection Prevention Cheat Sheet - Parameterized queries, stored procedures, and ORM usage patterns to prevent injection.
- OWASP Input Validation Cheat Sheet - Validation vs sanitization, allowlist vs denylist approaches, and context-specific encoding.
- OWASP Secure Headers Project - Reference for HTTP security headers, recommended values, and browser support.
- OWASP Rate Limiting Cheat Sheet - DoS prevention patterns including rate limiting, resource quotas, and circuit breakers.
- Fastify Security Guide - Official Fastify documentation on security practices, plugin recommendations, and common pitfalls.
- Fastify CORS Plugin Documentation -
@fastify/corsconfiguration reference including dynamic origin validation and preflight handling. - Fastify Helmet Plugin Documentation -
@fastify/helmetconfiguration for Content Security Policy, HSTS, and other security headers. - Fastify Rate Limit Plugin Documentation -
@fastify/rate-limitconfiguration including per-route limits, Redis backing, and custom key generators. - Fastify CSRF Protection Plugin Documentation -
@fastify/csrf-protectionpatterns for both session-based and cookie-based CSRF mitigation. - Fastify Secure Session Plugin Documentation -
@fastify/secure-sessionencrypted cookie sessions using sodium-native. - Fastify Sensible Plugin Documentation -
@fastify/sensibleerror handling utilities and security-relevant response helpers. joseLibrary Documentation - JWT/JWE/JWS/JWK implementation for Node.js with algorithm whitelisting and JWKS support.joseAPI Reference - DetailedjwtVerify()API documentation including all verification options.- Prisma Security Best Practices - Parameterized query patterns and raw query safety in Prisma ORM.
- Drizzle ORM SQL Injection Prevention - Safe SQL query building patterns with Drizzle's tagged template literals.
- Node.js Permission Model Documentation - Experimental permission system for filesystem, network, and child process restrictions.
- helmet.js Documentation - Comprehensive documentation for all helmet middleware headers and configuration options.
- DOMPurify Documentation - HTML sanitization library for preventing XSS in user-generated content.
- npm Audit Documentation - Built-in vulnerability scanning for npm dependencies.
- Socket.dev Security Platform - Supply chain attack detection analyzing dependency behavior rather than just known CVEs.
- lockfile-lint Documentation - Lockfile integrity verification to prevent lockfile manipulation attacks.
- pino Logger Documentation - Fastify's built-in structured logger with support for redaction and serializers.
- OWASP Top 10 for LLM Applications - Security risks specific to AI/LLM-integrated applications.
- Node.js Security Best Practices (nodejs.org) - Official Node.js security guidance including HTTP server hardening.
- NIST SP 800-63B Digital Identity Guidelines - Authentication and session management standards referenced by OWASP.
- RFC 7519 - JSON Web Token - JWT specification defining claims, structure, and security considerations.
- RFC 7518 - JSON Web Algorithms - JWA specification defining algorithm identifiers and requirements.
- RFC 6749 - OAuth 2.0 Authorization Framework - OAuth 2.0 specification relevant to token-based authentication flows.
- RFC 7636 - PKCE for OAuth 2.0 - Proof Key for Code Exchange, required for public OAuth clients.
- MDN Web Docs - HTTP Headers - Reference for security-related HTTP headers (CSP, HSTS, CORS, etc.).
- MDN Web Docs - SameSite Cookies - Browser behavior for SameSite cookie attribute values.
- MDN Web Docs - Content Security Policy - CSP reference including directive descriptions and examples.
- Snyk Security Advisories - Vulnerability database for npm packages with remediation guidance.
- CWE-89: SQL Injection - Common Weakness Enumeration entry for SQL injection with detailed attack patterns.
- CWE-79: Cross-site Scripting - CWE entry for XSS covering stored, reflected, and DOM-based variants.
- CWE-352: Cross-Site Request Forgery - CWE entry for CSRF with attack scenarios and mitigations.
- Auth0 JWT Security Best Practices - Analysis of JWT Best Current Practices RFC draft covering algorithm confusion and token binding.
- critical: algorithm confusion in jsonwebtoken (CVE-2022-23529) - Security advisory demonstrating why algorithm whitelisting is critical.
- SLSA Framework - Supply-chain Levels for Software Artifacts framework for build provenance and integrity.
- npm Provenance - npm provenance attestation documentation for supply chain security.
- Fastify v5 Release Notes - Breaking changes and security improvements in Fastify 5.
- libsodium Documentation - Cryptographic library underlying
@fastify/secure-sessionvia sodium-native bindings. - OWASP API Security Top 10 (2023) - API-specific security risks relevant to Fastify REST/GraphQL backends.
- Express to Fastify Migration Security Considerations - Security-relevant changes when migrating from Express middleware patterns.
- Content Security Policy Level 3 Specification - W3C specification for CSP including strict-dynamic and nonce patterns.
- Argon2 Password Hashing - Recommended password hashing algorithm (winner of Password Hashing Competition).
- bcrypt for Node.js - Widely-used password hashing library, alternative to Argon2.
- secure-json-parse - Fastify's JSON parser with prototype pollution protection, used internally.
- Husky Git Hooks - Git hook manager for running security checks on pre-commit and pre-push.
- lint-staged - Run linters/security checks on staged files only, paired with Husky.
- OWASP Dependency-Check - Dependency vulnerability scanner supporting multiple ecosystems.
- isomorphic-dompurify - Server-side compatible DOMPurify wrapper for Node.js sanitization.
- Fastify Autoload Plugin - Plugin for auto-loading routes and plugins, relevant to scaffold structure.
- Node.js Threat Model - Community security checklist for Node.js applications.
- OWASP Secure Coding Practices - Quick reference for secure coding patterns applicable to code generators.
- RFC 9110 - HTTP Semantics - HTTP specification sections on CORS, caching, and security considerations.
- Fastify Plugin System Architecture - How Fastify plugin encapsulation affects security boundary management.
- OWASP Testing Guide - Comprehensive security testing methodology applicable to scaffold test suites.
- npm best practices for security - Ten npm security practices from Snyk including lockfile management and script safety.
- CORS Specification (Fetch Living Standard) - WHATWG specification for CORS including preflight and credentialed requests.
- SRI (Subresource Integrity) - MDN documentation on SRI for verifying fetched resource integrity.
- Node.js --disable-proto Flag - CLI flag for disabling
__proto__access to prevent prototype pollution. - Fastify Logging with Pino - Built-in logging configuration including redaction and serializers.
- OWASP Logging Cheat Sheet - What to log, what not to log, and audit trail requirements.
- Doppler Secrets Management - Modern secrets management platform with Node.js SDK for production deployments.
- AWS Secrets Manager + Node.js - Cloud secrets management integration for production Fastify applications.
- OWASP SSRF Prevention Cheat Sheet - Patterns for preventing server-side request forgery in backend applications.
- Fastify Request Validation - JSON Schema validation and serialization documentation for input/output security.
- OWASP Password Storage Cheat Sheet - Password hashing recommendations including Argon2id, bcrypt, and scrypt parameters.
- OWASP Transport Layer Security Cheat Sheet - TLS configuration including cipher suites, certificate pinning, and HSTS.
- CVE-2022-23529 - jsonwebtoken arbitrary code execution - Critical vulnerability in jsonwebtoken affecting secret/public key handling.
- CVE-2022-23540 - jsonwebtoken insecure default algorithm - Vulnerability allowing weak algorithms in jsonwebtoken verification.
- OWASP Unvalidated Redirects and Forwards Cheat Sheet - Open redirect prevention patterns.
- Fastify Error Handling - Error handler customization for preventing information leakage.
- OWASP Error Handling Cheat Sheet - Error response patterns that prevent information disclosure.
- npm force resolutions - Package.json overrides for forcing secure dependency versions.
- Sigstore for npm - Cryptographic signing for npm packages providing build provenance.
- OWASP Cheat Sheet Series - Master index of all OWASP cheat sheets referenced throughout this research.
- Fastify Bearer Auth Plugin - Bearer token authentication plugin for simple API key/token validation.
- OWASP Authorization Cheat Sheet - Role-based and attribute-based access control patterns.
- Node.js PBKDF2 Considerations - Built-in key derivation for cases where Argon2 is not available.
- Fastify Type Provider (Zod, Typebox) - Type-safe validation as an alternative to raw JSON Schema.
- Zod Schema Validation - TypeScript-first schema validation library increasingly used with Fastify for input security.
- OWASP Mass Assignment Cheat Sheet - Prevention of mass assignment attacks through allowlisted fields.
- Fastify Decorators for RBAC - Using Fastify decorators to implement role-based access control.
- undici Fetch for Node.js - Node.js HTTP client with SSRF-relevant configuration options.
- better-npm-audit - Enhanced npm audit with exception allowlisting for CI/CD integration.
- OWASP Application Security Verification Standard (ASVS) - Comprehensive security verification requirements for web applications.
- Fastify Under Pressure Plugin - Load shedding and health check plugin for DoS resilience.
- Node.js Security Working Group - Official Node.js security working group resources and threat models.
Research Metadata
- Date Researched: 2026-02-10
- Category: dev
- Research Size: Deep (100) -- 10 agents, ~100 sources target
- Unique Sources: 96
- Approach: Adaptive 5-wave research (web access unavailable; synthesized from training knowledge spanning OWASP documentation, Fastify ecosystem docs, jose library reference, Node.js security guides, RFC specifications, and CVE databases)
- Search Queries Used:
- secure backend scaffold defaults best practices Node.js Fastify
- JWT verification jose library vs base64 decoding security
- httpOnly cookie session management Fastify secure-session
- CSRF protection @fastify/csrf-protection patterns
- @fastify/rate-limit per-route per-user sliding window configuration
- Content Security Policy helmet Fastify CORS configuration
- SQL injection prevention ORM input sanitization Node.js
- OWASP Top 10 mitigation code generation scaffolds
- Fastify security plugins registration order best practices
- supply chain security npm audit lockfile-lint provenance
- Node.js permission model experimental security flags
- Fastify error handling information leakage prevention