Security is Critical
Node.js applications are often exposed to the internet, making security paramount. This guide covers the most important security practices to protect your applications from common vulnerabilities.
🔐 OWASP Top 10 for Node.js
1. Injection (SQL, NoSQL, Command)
2. Broken Authentication
3. Sensitive Data Exposure
4. XML External Entities (XXE)
5. Broken Access Control
6. Security Misconfiguration
7. Cross-Site Scripting (XSS)
8. Insecure Deserialization
9. Using Components with Vulnerabilities
10. Insufficient Logging
Input Validation
const Joi = require('joi');
const express = require('express');
const app = express();
// Define validation schema
const userSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/).required(),
age: Joi.number().integer().min(13).max(120)
});
// Validation middleware
function validate(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true // Remove unknown fields
});
if (error) {
return res.status(400).json({
error: 'Validation failed',
details: error.details.map(d => d.message)
});
}
req.body = value; // Use sanitized value
next();
};
}
app.post('/users', validate(userSchema), (req, res) => {
// req.body is now validated and sanitized
createUser(req.body);
});
Preventing Injection Attacks
// SQL Injection Prevention
// BAD: Concatenating user input
const query = `SELECT * FROM users WHERE id = ${userId}`;
// GOOD: Use parameterized queries
const { Pool } = require('pg');
const pool = new Pool();
// PostgreSQL
const result = await pool.query(
'SELECT * FROM users WHERE id = $1',
[userId]
);
// MySQL
const mysql = require('mysql2/promise');
const [rows] = await connection.execute(
'SELECT * FROM users WHERE id = ?',
[userId]
);
// MongoDB Injection Prevention
// BAD: User can inject operators
const user = await User.findOne({ username: req.body.username });
// If username = { "$gt": "" }, returns first user!
// GOOD: Validate type
const username = String(req.body.username);
const user = await User.findOne({ username });
// Or use mongoose schema validation
const userSchema = new mongoose.Schema({
username: { type: String, required: true }
});
// Command Injection Prevention
// BAD: Shell command with user input
const { exec } = require('child_process');
exec(`ls ${userInput}`); // Dangerous!
// GOOD: Use execFile with arguments array
const { execFile } = require('child_process');
execFile('ls', [userInput], (err, stdout) => {
// Safe: arguments are not shell-interpreted
});
Using Helmet for HTTP Headers
const express = require('express');
const helmet = require('helmet');
const app = express();
// Use all default protections
app.use(helmet());
// Or configure individually
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));
// Headers helmet sets:
// X-Content-Type-Options: nosniff
// X-Frame-Options: DENY
// X-XSS-Protection: 0 (deprecated, CSP is better)
// Strict-Transport-Security: max-age=...
// Content-Security-Policy: ...
// And more...
Rate Limiting
const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');
const RedisStore = require('rate-limit-redis');
const { createClient } = require('redis');
// Basic rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: { error: 'Too many requests, please try again later' },
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', limiter);
// Stricter limits for auth endpoints
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 login attempts per hour
skipSuccessfulRequests: true,
});
app.use('/api/login', authLimiter);
// Distributed rate limiting with Redis
const redisClient = createClient({ url: 'redis://localhost:6379' });
const distributedLimiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redisClient.sendCommand(args),
}),
windowMs: 15 * 60 * 1000,
max: 100,
});
// Slow down instead of blocking
const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000,
delayAfter: 50, // Allow 50 requests, then slow down
delayMs: 500, // Add 500ms delay per request after limit
});
app.use(speedLimiter);
HTTPS and Secure Cookies
const https = require('https');
const fs = require('fs');
const express = require('express');
const app = express();
// Force HTTPS in production
if (process.env.NODE_ENV === 'production') {
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
res.redirect(`https://${req.header('host')}${req.url}`);
} else {
next();
}
});
}
// Secure cookie settings
const session = require('express-session');
app.use(session({
secret: process.env.SESSION_SECRET,
name: 'sessionId', // Don't use default 'connect.sid'
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only
httpOnly: true, // No JavaScript access
sameSite: 'strict', // CSRF protection
maxAge: 24 * 60 * 60 * 1000 // 24 hours
},
resave: false,
saveUninitialized: false
}));
// HTTPS server
const options = {
key: fs.readFileSync('private-key.pem'),
cert: fs.readFileSync('certificate.pem')
};
https.createServer(options, app).listen(443);
Dependency Security
# Check for vulnerabilities
npm audit
# Fix vulnerabilities automatically
npm audit fix
# Force fix (may include breaking changes)
npm audit fix --force
# Use Snyk for deeper analysis
npm install -g snyk
snyk test
snyk monitor # Continuous monitoring
# In package.json - lock versions
{
"dependencies": {
"express": "4.18.2" // Exact version, not ^4.18.2
}
}
# Use .npmrc for security
# .npmrc
audit=true
package-lock=true
ignore-scripts=true # Don't run postinstall scripts
Environment Variables & Secrets
// NEVER commit secrets to git!
// Use .env files (add to .gitignore)
// .env
DATABASE_URL=postgres://user:pass@localhost/db
JWT_SECRET=super-secret-key-change-me
API_KEY=sk-1234567890
// Load with dotenv
require('dotenv').config();
// Access secrets
const dbUrl = process.env.DATABASE_URL;
// Validate required env vars at startup
const required = ['DATABASE_URL', 'JWT_SECRET'];
for (const key of required) {
if (!process.env[key]) {
console.error(`Missing required env var: ${key}`);
process.exit(1);
}
}
// Use a secrets manager in production
// AWS Secrets Manager, HashiCorp Vault, etc.
const { SecretsManager } = require('@aws-sdk/client-secrets-manager');
async function getSecret(secretName) {
const client = new SecretsManager();
const response = await client.getSecretValue({ SecretId: secretName });
return JSON.parse(response.SecretString);
}
Security Checklist
- ☐ Use HTTPS everywhere
- ☐ Validate and sanitize all user input
- ☐ Use parameterized queries for databases
- ☐ Implement rate limiting
- ☐ Set secure HTTP headers with Helmet
- ☐ Use secure, httpOnly, sameSite cookies
- ☐ Run npm audit regularly
- ☐ Keep dependencies updated
- ☐ Never commit secrets to git
- ☐ Implement proper logging and monitoring
- ☐ Use strong password hashing (bcrypt, argon2)
- ☐ Implement proper error handling (don't leak info)
💡 Best Practices
- • Defense in depth: multiple security layers
- • Principle of least privilege: minimal permissions
- • Fail securely: errors should not expose info
- • Keep security in mind from the start
- • Regular security audits and penetration testing