Security Best Practices
Node.js Security Best Practices
Security is critical for any production application. Node.js applications face various security threats, from injection attacks to dependency vulnerabilities. This lesson covers essential security practices to protect your applications.
OWASP Top 10 for Node.js
The Open Web Application Security Project (OWASP) maintains a list of the most critical web application security risks:
- Broken Access Control: Improper authorization checks
- Cryptographic Failures: Weak encryption, exposed secrets
- Injection: SQL, NoSQL, command injection
- Insecure Design: Missing security controls
- Security Misconfiguration: Default credentials, verbose errors
- Vulnerable Components: Outdated dependencies
- Authentication Failures: Weak passwords, session management
- Data Integrity Failures: Insecure deserialization
- Logging Failures: Insufficient monitoring
- Server-Side Request Forgery (SSRF): Unvalidated redirects
SQL Injection Prevention
SQL injection occurs when untrusted data is included in SQL queries without proper sanitization:
// BAD: Vulnerable to SQL injection
app.get('/users/:id', async (req, res) => {
const userId = req.params.id;
const query = `SELECT * FROM users WHERE id = ${userId}`;
// Attacker could send: /users/1 OR 1=1
const user = await db.query(query);
res.json(user);
});
// GOOD: Using parameterized queries (prepared statements)
app.get('/users/:id', async (req, res) => {
const userId = req.params.id;
const query = 'SELECT * FROM users WHERE id = ?';
const user = await db.query(query, [userId]);
res.json(user);
});
// BETTER: Using ORM (e.g., Sequelize, TypeORM)
app.get('/users/:id', async (req, res) => {
const user = await User.findByPk(req.params.id);
res.json(user);
});
// Input validation with express-validator
const { param, validationResult } = require('express-validator');
app.get('/users/:id',
param('id').isInt().toInt(),
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const user = await User.findByPk(req.params.id);
res.json(user);
}
);
NoSQL Injection Prevention
NoSQL databases like MongoDB are also vulnerable to injection attacks:
// BAD: Vulnerable to NoSQL injection
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Attacker could send: {username: {$gt: ""}, password: {$gt: ""}}
const user = await User.findOne({ username, password });
if (user) {
res.json({ success: true });
}
});
// GOOD: Sanitize inputs
const mongoSanitize = require('express-mongo-sanitize');
app.use(mongoSanitize());
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Validate input types
if (typeof username !== 'string' || typeof password !== 'string') {
return res.status(400).json({ error: 'Invalid input' });
}
const user = await User.findOne({ username });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Use bcrypt to compare passwords
const isValid = await bcrypt.compare(password, user.passwordHash);
if (isValid) {
res.json({ success: true });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
Cross-Site Scripting (XSS) Prevention
XSS attacks inject malicious scripts into web pages viewed by other users:
// Install helmet for security headers
const helmet = require('helmet');
app.use(helmet());
// Install xss-clean to sanitize user input
const xss = require('xss-clean');
app.use(xss());
// BAD: Rendering unsanitized user input
app.get('/profile/:username', async (req, res) => {
const user = await User.findOne({ username: req.params.username });
res.send(`<h1>${user.bio}</h1>`); // XSS vulnerability!
});
// GOOD: Escape HTML in templates
const escapeHtml = require('escape-html');
app.get('/profile/:username', async (req, res) => {
const user = await User.findOne({ username: req.params.username });
res.send(`<h1>${escapeHtml(user.bio)}</h1>`);
});
// BETTER: Use templating engine that auto-escapes
app.set('view engine', 'ejs'); // or pug, handlebars, etc.
app.get('/profile/:username', async (req, res) => {
const user = await User.findOne({ username: req.params.username });
res.render('profile', { user }); // EJS auto-escapes by default
});
// Content Security Policy (CSP)
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'trusted-cdn.com'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
}
}));
Cross-Site Request Forgery (CSRF) Protection
CSRF attacks trick authenticated users into performing unwanted actions:
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
app.use(cookieParser());
// CSRF protection middleware
const csrfProtection = csrf({ cookie: true });
// Add CSRF token to forms
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
// Validate CSRF token on POST
app.post('/submit', csrfProtection, (req, res) => {
// CSRF token is automatically validated
res.json({ success: true });
});
// For API routes, use double-submit cookie pattern
app.use((req, res, next) => {
if (req.method === 'POST' || req.method === 'PUT' || req.method === 'DELETE') {
const token = req.headers['x-csrf-token'];
const cookieToken = req.cookies.csrfToken;
if (!token || token !== cookieToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
}
next();
});
Dependency Auditing
Vulnerable dependencies are a major security risk. Regularly audit and update your packages:
// Run npm audit to check for vulnerabilities
npm audit
// Fix vulnerabilities automatically (if possible)
npm audit fix
// Force fix (may introduce breaking changes)
npm audit fix --force
// Generate detailed audit report
npm audit --json > audit-report.json
// Use Snyk for continuous monitoring
npm install -g snyk
snyk auth
snyk test // Test for vulnerabilities
snyk monitor // Monitor project continuously
snyk wizard // Interactive vulnerability fixing
// Add to package.json scripts
{
"scripts": {
"audit": "npm audit",
"audit:fix": "npm audit fix",
"security:check": "snyk test"
}
}
// Use npm-check-updates to keep dependencies current
npm install -g npm-check-updates
ncu // Check for updates
ncu -u // Update package.json
npm install // Install updated packages
Security Headers
HTTP security headers protect against common attacks:
const helmet = require('helmet');
// Use helmet with custom configuration
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'trusted-cdn.com'"],
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
},
noSniff: true, // X-Content-Type-Options: nosniff
frameguard: { // X-Frame-Options: DENY
action: 'deny'
},
xssFilter: true, // X-XSS-Protection: 1; mode=block
referrerPolicy: {
policy: 'strict-origin-when-cross-origin'
}
}));
// Additional security headers
app.use((req, res, next) => {
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
res.setHeader('X-Powered-By', ''); // Remove X-Powered-By header
next();
});
Environment Variable Security
Protect sensitive configuration data:
// Use dotenv for local development only
if (process.env.NODE_ENV !== 'production') {
require('dotenv').config();
}
// Validate required environment variables
const requiredEnvVars = [
'DATABASE_URL',
'JWT_SECRET',
'API_KEY'
];
requiredEnvVars.forEach((varName) => {
if (!process.env[varName]) {
throw new Error(`Environment variable ${varName} is required`);
}
});
// Use strong secrets (minimum 32 characters)
const crypto = require('crypto');
// Generate a secure random secret
const generateSecret = () => {
return crypto.randomBytes(32).toString('hex');
};
console.log('Generated secret:', generateSecret());
// Store secrets in secure vaults (production)
// - AWS Secrets Manager
// - Azure Key Vault
// - HashiCorp Vault
// - Google Secret Manager
// Example with 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');
HTTPS Setup
Always use HTTPS in production to encrypt data in transit:
const https = require('https');
const fs = require('fs');
// Load SSL certificates
const privateKey = fs.readFileSync('/path/to/private-key.pem', 'utf8');
const certificate = fs.readFileSync('/path/to/certificate.pem', 'utf8');
const ca = fs.readFileSync('/path/to/ca-bundle.pem', 'utf8');
const credentials = {
key: privateKey,
cert: certificate,
ca: ca
};
// Create HTTPS server
const httpsServer = https.createServer(credentials, app);
httpsServer.listen(443, () => {
console.log('HTTPS Server running on port 443');
});
// Redirect HTTP to HTTPS
const http = require('http');
http.createServer((req, res) => {
res.writeHead(301, { Location: `https://${req.headers.host}${req.url}` });
res.end();
}).listen(80);
// Force HTTPS in Express
app.use((req, res, next) => {
if (req.secure || req.headers['x-forwarded-proto'] === 'https') {
next();
} else {
res.redirect(301, `https://${req.headers.host}${req.url}`);
}
});
Rate Limiting
Protect against brute-force and DDoS attacks:
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
// General rate limiter
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
app.use(generalLimiter);
// Strict limiter for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true, // Don't count successful logins
message: 'Too many login attempts, please try again later'
});
app.post('/login', authLimiter, async (req, res) => {
// Login logic
});
// Use Redis for distributed rate limiting
const redisClient = require('redis').createClient({
host: 'localhost',
port: 6379
});
const distributedLimiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:',
}),
windowMs: 15 * 60 * 1000,
max: 100
});
app.use(distributedLimiter);
Secure Password Hashing
const bcrypt = require('bcrypt');
// Hash password before storing
app.post('/register', async (req, res) => {
const { username, password } = req.body;
// Validate password strength
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
if (!passwordRegex.test(password)) {
return res.status(400).json({
error: 'Password must be at least 8 characters with uppercase, lowercase, number, and special character'
});
}
// Hash password with bcrypt (cost factor: 10-12 recommended)
const saltRounds = 12;
const passwordHash = await bcrypt.hash(password, saltRounds);
// Store user with hashed password
const user = await User.create({
username,
passwordHash
});
res.json({ success: true });
});
// Verify password during login
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate JWT token
const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ token });
});
- Never store plaintext passwords
- Use bcrypt, argon2, or scrypt (not MD5 or SHA1)
- Use salt rounds between 10-12 for bcrypt
- Enforce strong password policies (minimum 8 characters, complexity requirements)
- Implement account lockout after failed login attempts
- Use two-factor authentication (2FA) for sensitive accounts
Practice Exercise
Task: Secure a vulnerable Node.js application:
- Run npm audit and fix all vulnerabilities
- Implement helmet for security headers
- Add input validation with express-validator
- Implement CSRF protection
- Add rate limiting for auth endpoints
- Set up HTTPS with Let's Encrypt
- Implement secure password hashing with bcrypt
- Add MongoDB sanitization
- Configure Content Security Policy
- Test security with OWASP ZAP or Burp Suite
- ✓ Use HTTPS everywhere
- ✓ Keep dependencies updated (npm audit)
- ✓ Implement rate limiting
- ✓ Use security headers (helmet)
- ✓ Validate and sanitize all inputs
- ✓ Use parameterized queries
- ✓ Hash passwords with bcrypt
- ✓ Implement CSRF protection
- ✓ Set up logging and monitoring
- ✓ Use environment variables for secrets
- ✓ Implement authentication and authorization
- ✓ Regular security testing
- helmet: Security headers
- express-rate-limit: Rate limiting
- express-validator: Input validation
- bcrypt: Password hashing
- csurf: CSRF protection
- express-mongo-sanitize: NoSQL injection prevention
- xss-clean: XSS prevention
- hpp: HTTP parameter pollution prevention
- cors: CORS configuration
- snyk: Vulnerability scanning