Web Security Expert
This skill provides comprehensive expert knowledge of web application security for Node.js/Express applications, with emphasis on preventing common vulnerabilities, implementing defense-in-depth strategies, and following security best practices.
OWASP Top 10 Vulnerabilities
- Broken Access Control
What it is: Users can access resources or perform actions they shouldn't be authorized for.
Examples:
-
Accessing other users' data by changing URL parameters
-
Performing admin actions without admin privileges
-
Bypassing authentication by directly accessing protected pages
Prevention:
// BAD - No authorization check app.get('/api/users/:id/profile', async (req, res) => { const profile = await User.findById(req.params.id); res.json(profile); // Any user can access any profile! });
// GOOD - Check authorization app.get('/api/users/:id/profile', authenticate, async (req, res) => { // Verify user can only access their own profile if (req.user.id !== req.params.id && !req.user.isAdmin) { return res.status(403).json({ error: 'Access denied' }); }
const profile = await User.findById(req.params.id); res.json(profile); });
// BETTER - Middleware for resource ownership const requireOwnership = (resourceParam = 'id') => { return (req, res, next) => { if (req.user.id !== req.params[resourceParam] && !req.user.isAdmin) { return res.status(403).json({ error: 'Access denied' }); } next(); }; };
app.get('/api/users/:id/profile', authenticate, requireOwnership(), getProfile);
Best Practices:
-
Default deny all access, explicitly grant permissions
-
Verify authorization on every request (server-side)
-
Don't rely on client-side access control
-
Use role-based access control (RBAC) or attribute-based access control (ABAC)
-
Log access control failures for monitoring
- Cryptographic Failures
What it is: Exposure of sensitive data due to weak or missing encryption.
Examples:
-
Storing passwords in plain text
-
Using weak hashing algorithms (MD5, SHA1)
-
Transmitting sensitive data over HTTP
-
Hardcoding encryption keys
Prevention:
const bcrypt = require('bcrypt'); const crypto = require('crypto');
// Password Hashing (GOOD) const hashPassword = async (password) => { const saltRounds = 12; // Increase for more security (slower) return await bcrypt.hash(password, saltRounds); };
const verifyPassword = async (password, hash) => { return await bcrypt.compare(password, hash); };
// User registration app.post('/api/register', async (req, res) => { const { email, password } = req.body;
// Validate password strength if (password.length < 12) { return res.status(400).json({ error: 'Password must be at least 12 characters' }); }
const hashedPassword = await hashPassword(password);
const user = await User.create({ email, password: hashedPassword // NEVER store plain text });
res.status(201).json({ id: user.id }); });
// Encrypting Sensitive Data const algorithm = 'aes-256-gcm'; const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex'); // 32 bytes
const encrypt = (text) => { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return { encrypted, iv: iv.toString('hex'), authTag: authTag.toString('hex') }; };
const decrypt = (encrypted, iv, authTag) => { const decipher = crypto.createDecipheriv( algorithm, key, Buffer.from(iv, 'hex') );
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8');
return decrypted; };
Best Practices:
-
Use bcrypt, scrypt, or Argon2 for password hashing
-
Never use MD5 or SHA1 for passwords
-
Use TLS/SSL for all data in transit
-
Encrypt sensitive data at rest
-
Store encryption keys securely (environment variables, key management services)
-
Use strong random values for tokens and IDs
-
Implement key rotation
- Injection Attacks
What it is: Untrusted data is sent to an interpreter as part of a command or query.
Types:
-
SQL Injection
-
NoSQL Injection
-
Command Injection
-
LDAP Injection
-
XPath Injection
SQL Injection Prevention:
// BAD - Vulnerable to SQL injection
app.get('/api/users', async (req, res) => {
const username = req.query.username;
const query = SELECT * FROM users WHERE username = '${username}';
// If username = "admin' OR '1'='1", returns all users!
const users = await db.query(query);
res.json(users);
});
// GOOD - Use parameterized queries app.get('/api/users', async (req, res) => { const username = req.query.username; const query = 'SELECT * FROM users WHERE username = $1'; const users = await db.query(query, [username]); res.json(users); });
// GOOD - Use ORM/Query Builder app.get('/api/users', async (req, res) => { const username = req.query.username; const users = await User.findAll({ where: { username } // Sequelize automatically parameterizes }); res.json(users); });
NoSQL Injection Prevention:
// BAD - Vulnerable to NoSQL injection app.post('/api/login', async (req, res) => { const { username, password } = req.body; // If req.body = {username: {$ne: null}, password: {$ne: null}} // This bypasses authentication! const user = await User.findOne({ username, password }); });
// GOOD - Validate input types app.post('/api/login', async (req, res) => { const { username, password } = req.body;
// Ensure inputs are strings if (typeof username !== 'string' || typeof password !== 'string') { return res.status(400).json({ error: 'Invalid input' }); }
const user = await User.findOne({ username });
if (!user || !(await bcrypt.compare(password, user.password))) { return res.status(401).json({ error: 'Invalid credentials' }); }
res.json({ token: generateToken(user) }); });
Command Injection Prevention:
// BAD - Vulnerable to command injection
app.get('/api/ping', (req, res) => {
const host = req.query.host;
const { exec } = require('child_process');
exec(ping -c 4 ${host}, (error, stdout) => {
// If host = "google.com; rm -rf /", executes both commands!
res.send(stdout);
});
});
// GOOD - Validate input and use safe alternatives app.get('/api/ping', (req, res) => { const host = req.query.host;
// Validate hostname format const hostnameRegex = /^[a-zA-Z0-9.-]+$/; if (!hostnameRegex.test(host)) { return res.status(400).json({ error: 'Invalid hostname' }); }
// Use parameterized execution const { execFile } = require('child_process'); execFile('ping', ['-c', '4', host], (error, stdout) => { if (error) { return res.status(500).json({ error: 'Ping failed' }); } res.send(stdout); }); });
// BETTER - Use a library instead const ping = require('ping');
app.get('/api/ping', async (req, res) => { const host = req.query.host;
// Validate hostname if (!isValidHostname(host)) { return res.status(400).json({ error: 'Invalid hostname' }); }
const result = await ping.promise.probe(host); res.json(result); });
Best Practices:
-
Always use parameterized queries or prepared statements
-
Validate input data types
-
Use ORMs with built-in protection
-
Never execute user input as code
-
Use allow-lists for validation when possible
-
Escape special characters when allow-lists aren't feasible
- Insecure Design
What it is: Missing or ineffective security controls by design.
Examples:
-
No rate limiting on sensitive endpoints
-
Missing multi-factor authentication
-
Weak password requirements
-
No account lockout after failed logins
Prevention:
const rateLimit = require('express-rate-limit'); const MongoStore = require('rate-limit-mongo');
// Rate limiting for authentication endpoints const loginLimiter = rateLimit({ store: new MongoStore({ uri: process.env.MONGO_URI, collectionName: 'rate-limit' }), windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 attempts per window message: 'Too many login attempts, please try again later', standardHeaders: true, legacyHeaders: false, skipSuccessfulRequests: true, // Don't count successful logins skipFailedRequests: false });
// Account lockout tracking const loginAttempts = new Map();
app.post('/api/login', loginLimiter, async (req, res) => { const { email, password } = req.body;
// Check if account is locked const attempts = loginAttempts.get(email) || { count: 0, lockedUntil: null };
if (attempts.lockedUntil && attempts.lockedUntil > Date.now()) {
const minutesLeft = Math.ceil((attempts.lockedUntil - Date.now()) / 60000);
return res.status(423).json({
error: Account locked. Try again in ${minutesLeft} minutes
});
}
const user = await User.findOne({ email });
if (!user || !(await bcrypt.compare(password, user.password))) { // Increment failed attempts attempts.count += 1;
// Lock account after 5 failed attempts for 30 minutes
if (attempts.count >= 5) {
attempts.lockedUntil = Date.now() + 30 * 60 * 1000;
}
loginAttempts.set(email, attempts);
return res.status(401).json({ error: 'Invalid credentials' });
}
// Reset attempts on successful login loginAttempts.delete(email);
const token = generateToken(user); res.json({ token }); });
// Password strength validation const validatePassword = (password) => { const errors = [];
if (password.length < 12) { errors.push('Password must be at least 12 characters'); }
if (!/[A-Z]/.test(password)) { errors.push('Password must contain at least one uppercase letter'); }
if (!/[a-z]/.test(password)) { errors.push('Password must contain at least one lowercase letter'); }
if (!/[0-9]/.test(password)) { errors.push('Password must contain at least one number'); }
if (!/[^A-Za-z0-9]/.test(password)) { errors.push('Password must contain at least one special character'); }
// Check against common passwords const commonPasswords = ['password123', '12345678', 'qwerty123']; if (commonPasswords.includes(password.toLowerCase())) { errors.push('Password is too common'); }
return errors; };
Best Practices:
-
Implement rate limiting on all sensitive endpoints
-
Require strong passwords (length, complexity, no common passwords)
-
Implement account lockout after failed login attempts
-
Use multi-factor authentication for sensitive operations
-
Design with "security by default" principle
-
Implement proper session management
-
Use CAPTCHA for public forms
- Security Misconfiguration
What it is: Missing security hardening, default configurations, verbose error messages.
Examples:
-
Default admin passwords
-
Directory listing enabled
-
Detailed error messages in production
-
Unnecessary features enabled
Prevention:
const express = require('express'); const helmet = require('helmet'); const app = express();
// Security headers with Helmet app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], // Avoid unsafe-inline in production styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'"], fontSrc: ["'self'"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"] } }, hsts: { maxAge: 31536000, // 1 year includeSubDomains: true, preload: true }, referrerPolicy: { policy: 'strict-origin-when-cross-origin' }, noSniff: true, // X-Content-Type-Options xssFilter: true, // X-XSS-Protection hidePoweredBy: true // Remove X-Powered-By header }));
// Disable directory listing app.use(express.static('public', { dotfiles: 'deny', index: false // Disable directory indexing }));
// Environment-based error handling app.use((err, req, res, next) => { // Log full error server-side console.error('Error:', err);
// Send sanitized error to client const isProd = process.env.NODE_ENV === 'production';
res.status(err.status || 500).json({ error: isProd ? 'Internal Server Error' : err.message, // Never expose stack traces in production ...(isProd ? {} : { stack: err.stack }) }); });
// Disable unnecessary HTTP methods app.use((req, res, next) => { const allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'];
if (!allowedMethods.includes(req.method)) { return res.status(405).json({ error: 'Method not allowed' }); }
next(); });
// Remove fingerprinting headers app.disable('x-powered-by');
Best Practices:
-
Use security headers (Helmet)
-
Disable directory listing
-
Remove version disclosure headers
-
Use environment variables for configuration
-
Never use default credentials
-
Disable unnecessary features and endpoints
-
Keep dependencies updated
-
Run security scans regularly
- Vulnerable and Outdated Components
What it is: Using libraries with known vulnerabilities.
Prevention:
Regular dependency auditing
npm audit npm audit fix
Check for outdated packages
npm outdated
Use automated tools
npm install -g npm-check-updates ncu -u # Update package.json npm install
Use Snyk for continuous monitoring
npm install -g snyk snyk test snyk monitor
Dependency management:
// package.json - Use exact versions or compatible ranges carefully { "dependencies": { "express": "^5.2.1", // Compatible updates (minor/patch) "helmet": "~7.1.0", // Patch updates only "axios": "1.13.2" // Exact version } }
Best Practices:
-
Run npm audit regularly
-
Update dependencies frequently
-
Remove unused dependencies
-
Use tools like Snyk or Dependabot
-
Review CVE databases
-
Monitor security advisories
-
Use lock files (package-lock.json)
- Identification and Authentication Failures
What it is: Broken authentication and session management.
Examples:
-
Weak password policies
-
Session fixation
-
Credential stuffing vulnerabilities
-
Missing MFA
Prevention:
const jwt = require('jsonwebtoken'); const crypto = require('crypto');
// Generate secure tokens const generateSecureToken = () => { return crypto.randomBytes(32).toString('hex'); };
// JWT-based authentication const JWT_SECRET = process.env.JWT_SECRET; // Store securely const JWT_EXPIRES_IN = '1h'; // Short-lived tokens
const generateToken = (user) => { return jwt.sign( { id: user.id, email: user.email, role: user.role }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN, issuer: 'myapp', audience: 'myapp-users' } ); };
const verifyToken = (token) => { try { return jwt.verify(token, JWT_SECRET, { issuer: 'myapp', audience: 'myapp-users' }); } catch (error) { return null; } };
// Authentication middleware const authenticate = (req, res, next) => { const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'No token provided' }); }
const token = authHeader.substring(7); const decoded = verifyToken(token);
if (!decoded) { return res.status(401).json({ error: 'Invalid or expired token' }); }
req.user = decoded; next(); };
// Session management with Redis const session = require('express-session'); const RedisStore = require('connect-redis').default; const { createClient } = require('redis');
const redisClient = createClient({ url: process.env.REDIS_URL }); redisClient.connect();
app.use(session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, name: 'sessionId', // Don't use default 'connect.sid' cookie: { secure: true, // HTTPS only httpOnly: true, // No JavaScript access sameSite: 'strict', maxAge: 1000 * 60 * 60 * 24 // 24 hours } }));
// Password reset with secure tokens const passwordResetTokens = new Map();
app.post('/api/forgot-password', async (req, res) => { const { email } = req.body; const user = await User.findOne({ email });
if (!user) { // Don't reveal if user exists return res.json({ message: 'If account exists, reset email sent' }); }
const resetToken = generateSecureToken(); const expires = Date.now() + 3600000; // 1 hour
passwordResetTokens.set(resetToken, { userId: user.id, expires });
// Send email with reset link await sendResetEmail(email, resetToken);
res.json({ message: 'If account exists, reset email sent' }); });
app.post('/api/reset-password', async (req, res) => { const { token, newPassword } = req.body;
const resetData = passwordResetTokens.get(token);
if (!resetData || resetData.expires < Date.now()) { return res.status(400).json({ error: 'Invalid or expired token' }); }
// Validate new password const errors = validatePassword(newPassword); if (errors.length > 0) { return res.status(400).json({ errors }); }
const hashedPassword = await hashPassword(newPassword); await User.updateOne( { _id: resetData.userId }, { password: hashedPassword } );
// Invalidate token passwordResetTokens.delete(token);
res.json({ message: 'Password reset successful' }); });
Best Practices:
-
Use strong password hashing (bcrypt, scrypt, Argon2)
-
Implement multi-factor authentication
-
Use secure session management
-
Generate cryptographically secure tokens
-
Implement proper password reset flow
-
Use short-lived tokens with refresh mechanism
-
Invalidate sessions on logout
-
Protect against brute force attacks
- Software and Data Integrity Failures
What it is: Code and infrastructure that doesn't protect against integrity violations.
Examples:
-
No verification of npm packages
-
Insecure CI/CD pipeline
-
Auto-updates without verification
-
Insecure deserialization
Prevention:
// Verify package integrity // Use package-lock.json and verify checksums npm ci --ignore-scripts // Don't run install scripts automatically
// Input validation for deserialization app.post('/api/data', express.json({ limit: '10mb' }), (req, res) => { // Validate structure before processing const schema = { name: 'string', age: 'number', email: 'string' };
for (const [key, expectedType] of Object.entries(schema)) {
if (typeof req.body[key] !== expectedType) {
return res.status(400).json({
error: Invalid type for ${key}
});
}
}
// Process validated data });
// Avoid eval() and similar dangerous functions // BAD app.get('/api/calc', (req, res) => { const result = eval(req.query.expression); // NEVER DO THIS res.json({ result }); });
// GOOD - Use safe alternatives const math = require('mathjs');
app.get('/api/calc', (req, res) => { try { const result = math.evaluate(req.query.expression, { // Restrict available functions }); res.json({ result }); } catch (error) { res.status(400).json({ error: 'Invalid expression' }); } });
Best Practices:
-
Use package-lock.json or npm shrinkwrap
-
Verify package signatures and checksums
-
Review code in dependencies
-
Implement code signing for releases
-
Use CI/CD with security checks
-
Never deserialize untrusted data without validation
-
Avoid eval(), Function(), and vm module with user input
- Security Logging and Monitoring Failures
What it is: Insufficient logging and monitoring to detect breaches.
Prevention:
const winston = require('winston'); const morgan = require('morgan');
// Winston logger configuration const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), defaultMeta: { service: 'api' }, transports: [ new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), new winston.transports.File({ filename: 'logs/combined.log' }), new winston.transports.File({ filename: 'logs/security.log', level: 'warn' }) ] });
// Security event logging const logSecurityEvent = (event, req, details = {}) => { logger.warn('Security Event', { event, ip: req.ip, userAgent: req.headers['user-agent'], user: req.user?.id || 'anonymous', path: req.path, method: req.method, ...details }); };
// Log authentication failures app.post('/api/login', async (req, res) => { const { email, password } = req.body; const user = await User.findOne({ email });
if (!user || !(await bcrypt.compare(password, user.password))) { logSecurityEvent('LOGIN_FAILED', req, { email }); return res.status(401).json({ error: 'Invalid credentials' }); }
logSecurityEvent('LOGIN_SUCCESS', req, { userId: user.id }); res.json({ token: generateToken(user) }); });
// Log authorization failures app.get('/api/admin', authenticate, requireAdmin, (req, res) => { res.json({ admin: true }); });
const requireAdmin = (req, res, next) => { if (req.user.role !== 'admin') { logSecurityEvent('AUTHORIZATION_FAILED', req, { requiredRole: 'admin', userRole: req.user.role }); return res.status(403).json({ error: 'Access denied' }); } next(); };
// Log data access app.get('/api/users/:id', authenticate, async (req, res) => { logSecurityEvent('USER_DATA_ACCESS', req, { targetUserId: req.params.id, accessorId: req.user.id });
const user = await User.findById(req.params.id); res.json(user); });
// HTTP request logging with Morgan app.use(morgan('combined', { stream: { write: (message) => logger.info(message.trim()) } }));
// Monitor for suspicious patterns const suspiciousActivityDetector = (req, res, next) => { // Detect SQL injection attempts const sqlPatterns = /'|--|;|/*|*/|xp_|sp_|UNION|SELECT|INSERT|DELETE|DROP/i; const params = JSON.stringify(req.query) + JSON.stringify(req.body);
if (sqlPatterns.test(params)) { logSecurityEvent('SUSPECTED_SQL_INJECTION', req, { params }); }
// Detect path traversal attempts if (req.path.includes('..') || req.path.includes('~')) { logSecurityEvent('SUSPECTED_PATH_TRAVERSAL', req); }
next(); };
app.use(suspiciousActivityDetector);
Best Practices:
-
Log all authentication events (success and failure)
-
Log authorization failures
-
Log input validation failures
-
Log security-relevant configuration changes
-
Include context (IP, user, timestamp, action)
-
Protect logs from tampering
-
Set up alerts for suspicious patterns
-
Retain logs for appropriate period
-
Don't log sensitive data (passwords, tokens, PII)
- Server-Side Request Forgery (SSRF)
What it is: Application fetches remote resources without validating user-supplied URLs.
Prevention:
const axios = require('axios'); const { URL } = require('url');
// URL validation const isValidUrl = (urlString) => { try { const url = new URL(urlString);
// Only allow HTTPS
if (url.protocol !== 'https:') {
return false;
}
// Block private IP ranges
const hostname = url.hostname;
// Block localhost
if (hostname === 'localhost' || hostname === '127.0.0.1') {
return false;
}
// Block private networks
const privateRanges = [
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
/^192\.168\./,
/^169\.254\./ // Link-local
];
if (privateRanges.some(pattern => pattern.test(hostname))) {
return false;
}
// Block cloud metadata endpoints
if (hostname === '169.254.169.254') {
return false;
}
return true;
} catch (error) { return false; } };
// Safe URL fetching app.post('/api/fetch-url', async (req, res) => { const { url } = req.body;
// Validate URL if (!isValidUrl(url)) { return res.status(400).json({ error: 'Invalid or unsafe URL' }); }
// Use allowlist if possible const allowedDomains = ['api.example.com', 'data.example.com']; const urlObj = new URL(url);
if (!allowedDomains.includes(urlObj.hostname)) { return res.status(403).json({ error: 'Domain not allowed' }); }
try { const response = await axios.get(url, { timeout: 5000, maxRedirects: 0, // Don't follow redirects maxContentLength: 1024 * 1024 // 1MB limit });
res.json({ data: response.data });
} catch (error) { res.status(500).json({ error: 'Failed to fetch URL' }); } });
// Proxy endpoint with strict validation app.post('/api/proxy', async (req, res) => { // Only allow specific API endpoints const allowedEndpoints = [ 'https://api.usaspending.gov/api/v2/search/spending_by_award/', 'https://api.usaspending.gov/api/v2/search/spending_by_award_count/' ];
const targetUrl = 'https://api.usaspending.gov/api/v2/search/spending_by_award/';
if (!allowedEndpoints.includes(targetUrl)) { return res.status(403).json({ error: 'Endpoint not allowed' }); }
try { const response = await axios.post(targetUrl, req.body, { headers: { 'Content-Type': 'application/json' }, timeout: 10000, maxRedirects: 0 });
res.json(response.data);
} catch (error) { res.status(error.response?.status || 500).json({ error: 'Proxy request failed' }); } });
Best Practices:
-
Validate and sanitize all URLs
-
Use allowlists for allowed domains
-
Block private IP ranges and localhost
-
Block cloud metadata endpoints (169.254.169.254)
-
Disable or limit redirects
-
Implement request timeouts
-
Use network segmentation
Input Validation and Sanitization
Validation Libraries
express-validator:
const { body, query, param, validationResult } = require('express-validator');
app.post('/api/users', // Validation chain body('email') .isEmail() .normalizeEmail() .withMessage('Invalid email'),
body('age') .isInt({ min: 18, max: 120 }) .withMessage('Age must be between 18 and 120'),
body('username') .isLength({ min: 3, max: 20 }) .matches(/^[a-zA-Z0-9_]+$/) .withMessage('Username must be 3-20 alphanumeric characters'),
body('website') .optional() .isURL() .withMessage('Invalid URL'),
async (req, res) => { const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Data is validated
const user = await User.create(req.body);
res.status(201).json(user);
} );
Joi:
const Joi = require('joi');
const userSchema = Joi.object({ email: Joi.string().email().required(), password: Joi.string().min(12).required(), age: Joi.number().integer().min(18).max(120), website: Joi.string().uri().optional(), tags: Joi.array().items(Joi.string()).max(10) });
const validateRequest = (schema) => { return (req, res, next) => { const { error, value } = schema.validate(req.body, { abortEarly: false, stripUnknown: true });
if (error) {
return res.status(400).json({
error: 'Validation failed',
details: error.details.map(d => ({
field: d.path.join('.'),
message: d.message
}))
});
}
req.body = value; // Use validated data
next();
}; };
app.post('/api/users', validateRequest(userSchema), async (req, res) => { const user = await User.create(req.body); res.status(201).json(user); });
Sanitization
const xss = require('xss'); const validator = require('validator');
// HTML sanitization const sanitizeHtml = (dirty) => { return xss(dirty, { whiteList: { p: [], br: [], strong: [], em: [], a: ['href'] } }); };
// Escape HTML entities const escapeHtml = (unsafe) => { return validator.escape(unsafe); };
app.post('/api/posts', async (req, res) => { const { title, content } = req.body;
const sanitizedPost = { title: escapeHtml(title), content: sanitizeHtml(content) };
const post = await Post.create(sanitizedPost); res.status(201).json(post); });
Secrets Management
Environment Variables
// .env file (NEVER commit to Git) JWT_SECRET=your-256-bit-secret-here DATABASE_URL=postgresql://user:pass@localhost:5432/db API_KEY=your-api-key-here ENCRYPTION_KEY=64-char-hex-string-here
// Load environment variables require('dotenv').config();
// Access secrets const jwtSecret = process.env.JWT_SECRET; const dbUrl = process.env.DATABASE_URL;
// Validate required secrets on startup const requiredSecrets = ['JWT_SECRET', 'DATABASE_URL'];
for (const secret of requiredSecrets) {
if (!process.env[secret]) {
console.error(FATAL: ${secret} environment variable is not set);
process.exit(1);
}
}
Cloud Secret Managers
Google Secret Manager:
const { SecretManagerServiceClient } = require('@google-cloud/secret-manager'); const client = new SecretManagerServiceClient();
async function getSecret(secretName) {
const [version] = await client.accessSecretVersion({
name: projects/${projectId}/secrets/${secretName}/versions/latest
});
return version.payload.data.toString('utf8'); }
// Usage const dbPassword = await getSecret('database-password');
AWS Secrets Manager:
const AWS = require('aws-sdk'); const secretsManager = new AWS.SecretsManager({ region: 'us-east-1' });
async function getSecret(secretName) { const data = await secretsManager.getSecretValue({ SecretId: secretName }).promise();
return JSON.parse(data.SecretString); }
// Usage const dbCredentials = await getSecret('prod/database/credentials');
Best Practices
-
Never hardcode secrets in source code
-
Never commit secrets to version control
-
Use environment variables or secret managers
-
Rotate secrets regularly
-
Use different secrets for different environments
-
Limit access to secrets (principle of least privilege)
-
Audit secret access
-
Encrypt secrets at rest and in transit
Security Headers
Complete Helmet Configuration
const helmet = require('helmet');
app.use(helmet({ // Content Security Policy contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "trusted-cdn.com"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'", "api.example.com"], fontSrc: ["'self'"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"], baseUri: ["'self'"], formAction: ["'self'"], frameAncestors: ["'none'"] } },
// HTTP Strict Transport Security hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
// X-Content-Type-Options noSniff: true,
// X-Frame-Options frameguard: { action: 'deny' },
// Referrer-Policy referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
// X-Download-Options ieNoOpen: true,
// X-DNS-Prefetch-Control dnsPrefetchControl: { allow: false },
// Remove X-Powered-By hidePoweredBy: true,
// X-Permitted-Cross-Domain-Policies permittedCrossDomainPolicies: { permittedPolicies: 'none' } }));
API Security Best Practices
API Key Management
const crypto = require('crypto');
// Generate API keys const generateApiKey = () => { return crypto.randomBytes(32).toString('hex'); };
// Middleware to validate API keys const validateApiKey = async (req, res, next) => { const apiKey = req.headers['x-api-key'];
if (!apiKey) { return res.status(401).json({ error: 'API key required' }); }
// Hash the provided key for comparison const hashedKey = crypto .createHash('sha256') .update(apiKey) .digest('hex');
// Look up in database const validKey = await ApiKey.findOne({ keyHash: hashedKey, active: true, expiresAt: { $gt: new Date() } });
if (!validKey) { return res.status(401).json({ error: 'Invalid API key' }); }
// Track usage await ApiKey.updateOne( { _id: validKey._id }, { $inc: { usageCount: 1 }, lastUsedAt: new Date() } );
req.apiKey = validKey; next(); };
app.get('/api/data', validateApiKey, (req, res) => { res.json({ data: [] }); });
Rate Limiting by API Key
const rateLimit = require('express-rate-limit');
const createApiLimiter = (maxRequests, windowMs) => { return rateLimit({ windowMs, max: maxRequests, keyGenerator: (req) => { // Use API key as rate limit key return req.headers['x-api-key'] || req.ip; }, handler: (req, res) => { res.status(429).json({ error: 'Rate limit exceeded', retryAfter: res.getHeader('Retry-After') }); } }); };
const apiLimiter = createApiLimiter(1000, 60 * 60 * 1000); // 1000/hour
app.use('/api', validateApiKey, apiLimiter);
Security Testing
Example Security Tests
const request = require('supertest'); const app = require('./server');
describe('Security Tests', () => { describe('SQL Injection', () => { it('should reject SQL injection in query params', async () => { const response = await request(app) .get("/api/users?id=1' OR '1'='1") .expect(400);
expect(response.body).toHaveProperty('error');
});
});
describe('XSS Protection', () => { it('should sanitize HTML in input', async () => { const response = await request(app) .post('/api/posts') .send({ title: '<script>alert("xss")</script>Test', content: 'Content' }) .expect(201);
expect(response.body.title).not.toContain('<script>');
});
});
describe('Authentication', () => { it('should reject requests without token', async () => { await request(app) .get('/api/protected') .expect(401); });
it('should reject invalid tokens', async () => {
await request(app)
.get('/api/protected')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
});
describe('Rate Limiting', () => { it('should enforce rate limits', async () => { const requests = Array(10).fill().map(() => request(app).post('/api/login').send({ email: 'test@example.com', password: 'password' }) );
const responses = await Promise.all(requests);
const tooManyRequests = responses.filter(r => r.status === 429);
expect(tooManyRequests.length).toBeGreaterThan(0);
});
});
describe('Authorization', () => { it('should prevent unauthorized access to resources', async () => { const userToken = generateToken({ id: 1, role: 'user' });
await request(app)
.get('/api/admin')
.set('Authorization', `Bearer ${userToken}`)
.expect(403);
});
}); });
Security Checklist
Pre-Deployment Checklist
-
All secrets stored in environment variables or secret managers
-
No secrets committed to version control
-
Security headers configured (Helmet)
-
HTTPS enforced in production
-
CORS properly configured
-
Rate limiting implemented on sensitive endpoints
-
Input validation on all user inputs
-
Output encoding to prevent XSS
-
Parameterized queries to prevent SQL injection
-
Strong password policies enforced
-
Account lockout after failed login attempts
-
Session management secure (httpOnly, secure, sameSite)
-
Error messages don't leak sensitive info
-
Dependencies audited and updated
-
Security logging implemented
-
File upload restrictions (type, size, location)
-
API authentication and authorization
-
SSRF protections for URL fetching
-
Command injection protections
-
Regular security testing
Resources
-
OWASP Top 10: https://owasp.org/www-project-top-ten/
-
OWASP Cheat Sheets: https://cheatsheetseries.owasp.org/
-
Node.js Security Best Practices: https://nodejs.org/en/docs/guides/security/
-
Express Security Best Practices: https://expressjs.com/en/advanced/best-practice-security.html
-
Snyk Vulnerability Database: https://security.snyk.io/
-
npm Security Advisories: https://www.npmjs.com/advisories