Node.js Security Best Practices: Protecting Your Applications
Node.js Security Best Practices: Protecting Your Applications
Security should be a top priority for every Node.js application. This guide covers essential security practices to protect your applications.
Input Validation
Using Joi
const Joi = require('joi');
const userSchema = Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(0).max(120),
password: Joi.string().min(8).pattern(/^(?=.*[A-Z])(?=.*[0-9])/),
});
app.post('/users', (req, res) => {
const { error, value } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
// Use validated value
createUser(value);
});
Using express-validator
const { body, validationResult } = require('express-validator');
app.post('/users',
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }),
body('name').trim().escape(),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process request
}
);
SQL Injection Prevention
Parameterized Queries
// Bad - SQL Injection vulnerability
app.get('/users', (req, res) => {
const query = `SELECT * FROM users WHERE id = ${req.query.id}`;
db.query(query);
});
// Good - Parameterized query
app.get('/users', async (req, res) => {
const result = await db.query(
'SELECT * FROM users WHERE id = $1',
[req.query.id]
);
res.json(result.rows);
});
// Using ORM (Prisma)
const user = await prisma.user.findUnique({
where: { id: parseInt(req.query.id) }
});
XSS Prevention
Helmet.js
const helmet = require('helmet');
app.use(helmet());
// Or configure individual headers
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "trusted-cdn.com"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "trusted-cdn.com"],
},
}));
app.use(helmet.xssFilter());
app.use(helmet.frameguard({ action: 'deny' }));
app.use(helmet.noSniff());
Sanitizing Output
const sanitizeHtml = require('sanitize-html');
app.post('/comments', (req, res) => {
const cleanComment = sanitizeHtml(req.body.comment, {
allowedTags: ['b', 'i', 'em', 'strong'],
allowedAttributes: {},
});
saveComment(cleanComment);
});
CSRF Protection
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
app.use(cookieParser());
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
app.post('/submit', csrfProtection, (req, res) => {
// Process form
});
Rate Limiting
const rateLimit = require('express-rate-limit');
// General limiter
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: 'Too many requests, please try again later.',
});
app.use(limiter);
// Strict limiter for auth routes
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 attempts per hour
skipSuccessfulRequests: true,
});
app.post('/login', authLimiter, loginHandler);
Secure Headers
app.use((req, res, next) => {
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// XSS protection
res.setHeader('X-XSS-Protection', '1; mode=block');
// Prevent MIME type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// HSTS
res.setHeader('Strict-Transport-Security',
'max-age=31536000; includeSubDomains');
// Disable caching for sensitive routes
res.setHeader('Cache-Control', 'no-store');
next();
});
Authentication Best Practices
Password Hashing
const bcrypt = require('bcrypt');
// Hashing
async function hashPassword(password) {
const salt = await bcrypt.genSalt(12);
return bcrypt.hash(password, salt);
}
// Verification
async function verifyPassword(password, hash) {
return bcrypt.compare(password, hash);
}
// Registration
app.post('/register', async (req, res) => {
const hashedPassword = await hashPassword(req.body.password);
await createUser({
email: req.body.email,
password: hashedPassword,
});
});
JWT Security
const jwt = require('jsonwebtoken');
// Generate token
function generateToken(user) {
return jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{
expiresIn: '1h',
issuer: 'your-app',
audience: 'your-app-users',
}
);
}
// Middleware
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
}
Refresh Token Pattern
const refreshTokens = new Map(); // Use Redis in production
app.post('/refresh', (req, res) => {
const refreshToken = req.body.refreshToken;
if (!refreshTokens.has(refreshToken)) {
return res.status(403).json({ error: 'Invalid refresh token' });
}
const user = refreshTokens.get(refreshToken);
const newToken = generateToken(user);
res.json({ token: newToken });
});
app.post('/logout', (req, res) => {
refreshTokens.delete(req.body.refreshToken);
res.json({ message: 'Logged out' });
});
Environment Variables
// .env file (never commit to git)
DATABASE_URL=postgresql://user:pass@localhost:5432/db
JWT_SECRET=your-super-secret-key
API_KEY=your-api-key
// config.js
require('dotenv').config();
module.exports = {
database: {
url: process.env.DATABASE_URL,
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: '1h',
},
};
// Validate required env vars
const requiredEnvVars = ['DATABASE_URL', 'JWT_SECRET'];
requiredEnvVars.forEach((envVar) => {
if (!process.env[envVar]) {
throw new Error(`Missing required env var: ${envVar}`);
}
});
Secure Dependencies
# Audit dependencies
npm audit
# Fix vulnerabilities
npm audit fix
# Check for outdated packages
npm outdated
# Use npm-force-resolutions for stubborn issues
// package.json
{
"scripts": {
"audit": "npm audit --audit-level=moderate",
"preinstall": "npm audit --audit-level=high || exit 0"
}
}
Error Handling
// Custom error class
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
}
}
// Error handler middleware
app.use((err, req, res, next) => {
// Log error (don't expose to client)
console.error(err);
// Don't leak error details in production
const message = process.env.NODE_ENV === 'production'
? 'Something went wrong'
: err.message;
res.status(err.statusCode || 500).json({
error: message,
// Never send stack traces to client
});
});
Logging and Monitoring
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
});
// Log security events
app.post('/login', async (req, res) => {
const user = await authenticate(req.body);
if (!user) {
logger.warn('Failed login attempt', {
email: req.body.email,
ip: req.ip,
userAgent: req.headers['user-agent'],
});
return res.status(401).json({ error: 'Invalid credentials' });
}
logger.info('User logged in', { userId: user.id });
res.json({ token: generateToken(user) });
});
Security Checklist
- Validate and sanitize all input
- Use parameterized queries
- Implement rate limiting
- Use HTTPS everywhere
- Set secure HTTP headers
- Hash passwords with bcrypt
- Implement proper session management
- Keep dependencies updated
- Log security events
- Implement CORS properly
- Use environment variables for secrets
- Implement proper error handling
Conclusion
Security is an ongoing process, not a one-time task. Regularly audit your code, keep dependencies updated, and stay informed about new vulnerabilities. Implement these practices from the start of your project.