'=');
}
static sanitizeUrl(input: string): string {
try {
const url = new URL(input, 'http://localhost');
// Only allow safe protocols
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
return '/blocked-resource';
}
return url.href;
} catch {
return '/invalid-url';
}
}
}
For database interactions, string concatenation is strictly prohibited. We enforce a `RepositoryPattern` that wraps all queries in prepared statements. This ensures the database driver handles escaping, rendering SQL injection impossible regardless of input content.
```typescript
// src/data/database-repository.ts
import { Pool, QueryResult } from 'pg';
export class UserRepository {
private pool: Pool;
constructor(pool: Pool) {
this.pool = pool;
}
// Parameterized query execution
async findByEmail(email: string): Promise<QueryResult | null> {
const query = 'SELECT id, email, password_hash, role FROM users WHERE email = $1';
// The driver safely binds parameters; input is never interpolated into SQL
const result = await this.pool.query(query, [email]);
return result.rows.length > 0 ? result : null;
}
async createRecord(data: { name: string; email: string }): Promise<string> {
const query = `
INSERT INTO users (name, email, created_at)
VALUES ($1, $2, NOW())
RETURNING id
`;
const result = await this.pool.query(query, [data.name, data.email]);
return result.rows[0].id;
}
}
2. Identity and Session Hardening
Password storage must use memory-hard, slow hashing algorithms. We utilize Argon2, which is resistant to GPU and ASIC brute-force attacks. Fast hashes like MD5 or SHA-256 are cryptographically broken for password storage and must never be used.
// src/security/auth-vault.ts
import argon2 from '@node-rs/argon2';
export class AuthVault {
static async hashCredential(plaintext: string): Promise<string> {
// Argon2id is the recommended variant
return argon2.hash(plaintext, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB
timeCost: 3,
parallelism: 4,
});
}
static async verifyCredential(plaintext: string, hash: string): Promise<boolean> {
return argon2.verify(hash, plaintext);
}
}
Session management requires strict cookie attributes. We configure sessions to use the __Host- prefix for domain isolation, enforce Secure and HttpOnly flags, and disable rolling sessions to prevent fixation attacks. For API authentication, we implement a token rotation pattern where refresh tokens are single-use and stored server-side for revocation capability.
// src/security/session-manager.ts
import session from 'express-session';
import crypto from 'crypto';
export const createSecureSessionMiddleware = () => {
const secret = process.env.SESSION_SECRET;
if (!secret || secret.length < 32) {
throw new Error('SESSION_SECRET must be at least 32 characters');
}
return session({
name: '__Host-sid', // Prefix enforces secure scope
secret: secret,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JS access
sameSite: 'strict', // CSRF mitigation
maxAge: 3600000, // 1 hour absolute expiry
path: '/',
},
// Rolling disabled to prevent session fixation
rolling: false,
});
};
// Token rotation service for JWT-based auth
export class TokenRotator {
static generatePair(userId: string, roles: string[]) {
const accessToken = {
sub: userId,
roles,
exp: Math.floor(Date.now() / 1000) + 900, // 15 minutes
};
const refreshToken = {
sub: userId,
jti: crypto.randomUUID(), // Unique ID for revocation
exp: Math.floor(Date.now() / 1000) + 604800, // 7 days
};
return { accessToken, refreshToken };
}
}
3. Request Integrity and Transport Security
Cross-Site Request Forgery (CSRF) is mitigated via SameSite cookies for session-based auth. For APIs requiring custom headers, we implement a Double-Submit Cookie pattern where a CSRF token is sent in both a cookie and a request header, validated by the server.
Rate limiting protects against brute-force attacks and resource exhaustion. We implement a sliding window algorithm to provide smoother throttling than fixed windows.
// src/security/rate-limiter.ts
export class SlidingWindowLimiter {
private windows: Map<string, { count: number; resetTime: number }>;
private windowMs: number;
private maxRequests: number;
constructor(windowMs: number, maxRequests: number) {
this.windows = new Map();
this.windowMs = windowMs;
this.maxRequests = maxRequests;
}
isAllowed(key: string): boolean {
const now = Date.now();
const window = this.windows.get(key);
if (!window || now > window.resetTime) {
this.windows.set(key, { count: 1, resetTime: now + this.windowMs });
return true;
}
if (window.count >= this.maxRequests) {
return false;
}
window.count++;
return true;
}
}
// Security headers configuration
export const applySecurityHeaders = (app: any) => {
app.use((req: any, res: any, next: any) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"frame-ancestors 'none'",
"form-action 'self'",
].join('; '));
next();
});
};
Pitfall Guide
Even with robust tools, implementation errors can reintroduce vulnerabilities. The following pitfalls represent common production failures and their remedies.
-
The "Fast Hash" Trap
- Explanation: Using MD5, SHA-1, or SHA-256 for password hashing. These algorithms are designed for speed, allowing attackers to compute billions of guesses per second using GPUs.
- Fix: Always use Argon2id, bcrypt, or scrypt. These are intentionally slow and memory-hard, making brute-force attacks computationally prohibitive.
-
Context-Agnostic Encoding
- Explanation: Applying HTML encoding to data that is later inserted into a JavaScript block or HTML attribute. An attacker can break out of the context if the encoding doesn't match the insertion point.
- Fix: Use context-aware sanitization. Encode differently for HTML bodies, attributes, URLs, and JavaScript contexts. Prefer template engines that auto-escape based on context.
-
Session Fixation via Rolling
- Explanation: Enabling
rolling: true in session middleware extends the session lifetime on every request. This can be exploited in fixation attacks where an attacker sets a known session ID and keeps it alive indefinitely.
- Fix: Disable rolling sessions. Use absolute expiration (
maxAge) and regenerate session IDs upon privilege escalation (e.g., login).
-
Over-Permissive CSP
- Explanation: Adding
'unsafe-inline' to script-src to fix broken functionality. This nullifies the protection against XSS, as injected scripts can still execute.
- Fix: Remove
'unsafe-inline'. Use nonces or hashes for inline scripts, or move all logic to external files. Refactor code to comply with strict CSP.
-
Information Leakage in Auth Errors
- Explanation: Returning distinct messages for "User not found" vs. "Invalid password." This allows attackers to enumerate valid accounts.
- Fix: Return a generic message like "Invalid credentials" for all authentication failures. Implement rate limiting to mitigate enumeration attempts.
-
Weak Session Secrets
- Explanation: Using predictable or short secrets for session signing. Attackers can forge session cookies if the secret is compromised.
- Fix: Generate secrets using a cryptographically secure random number generator. Ensure secrets are at least 32 bytes and stored in environment variables, never in code.
-
Missing SameSite Attributes
- Explanation: Omitting
SameSite on cookies allows cross-origin requests to include session cookies, enabling CSRF attacks.
- Fix: Set
SameSite: 'strict' or 'lax' on all session cookies. This is the primary defense against CSRF for cookie-based authentication.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Monolithic Web App | Server-Side Sessions | Simpler revocation, built-in CSRF protection via cookies, no token management overhead. | Low (Session store required) |
| Mobile/SPA API | JWT with Rotation | Stateless auth scales better, works across domains, but requires rotation logic for security. | Medium (Rotation/Revocation logic) |
| High-Security Admin | Short-Lived JWT + MFA | Minimizes window of compromise; MFA adds defense-in-depth for privileged actions. | High (UX friction, MFA infra) |
| Public Read-Only API | API Keys + Rate Limiting | No session overhead, easy key rotation, rate limiting prevents abuse. | Low (Key management) |
Configuration Template
This template consolidates security middleware into a reusable stack. Copy this into your application setup to enforce baseline security controls.
// src/config/security-stack.ts
import { Application } from 'express';
import { createSecureSessionMiddleware } from '../security/session-manager';
import { applySecurityHeaders } from '../security/rate-limiter';
import { SlidingWindowLimiter } from '../security/rate-limiter';
export const applySecurityStack = (app: Application) => {
// 1. Security Headers
applySecurityHeaders(app);
// 2. Session Management
app.use(createSecureSessionMiddleware());
// 3. Rate Limiting (Login Endpoint)
const loginLimiter = new SlidingWindowLimiter(15 * 60 * 1000, 5);
app.post('/api/auth/login', (req, res, next) => {
const ip = req.ip;
if (!loginLimiter.isAllowed(`login:${ip}`)) {
return res.status(429).json({ error: 'Too many attempts. Try again later.' });
}
next();
});
// 4. Global API Rate Limiting
const apiLimiter = new SlidingWindowLimiter(60 * 1000, 100);
app.use('/api/', (req, res, next) => {
const key = req.user?.id || req.ip;
if (!apiLimiter.isAllowed(`api:${key}`)) {
return res.status(429).json({ error: 'Rate limit exceeded.' });
}
next();
});
};
Quick Start Guide
- Install Dependencies: Add
@node-rs/argon2, express-session, and pg (or your DB driver) to your project.
- Apply Security Stack: Import and invoke
applySecurityStack(app) early in your application initialization, before routes.
- Configure Environment: Set
SESSION_SECRET to a 32+ character random string. Ensure NODE_ENV is set to production to enforce secure cookies.
- Refactor Database Access: Replace all raw SQL string concatenation with parameterized queries using your repository pattern.
- Verify Headers: Use a tool like
securityheaders.com or browser dev tools to confirm CSP, HSTS, and X-Content-Type-Options are present and correctly configured.