Node.js Security Best Practices
Harden your Node.js applications against common vulnerabilities including injection, broken authentication, and dependency risks.
SecurityAdvanced20 min read
AI Summary
Quick context for Node.js Security Best Practices
Node.js Security Best Practices is a advanced guide in Security. Harden your Node.js applications against common vulnerabilities including injection, broken authentication, and dependency risks.. Key topics: authentication, encryption, nodejs, security.
## Security Mindset
Security is not a feature you add at the end. It is a practice woven into every line of code. This guide covers the vulnerabilities that matter most in Node.js applications and how to prevent them.
## Input Validation
Never trust user input. Validate and sanitize everything at the boundary.
```javascript
// Using zod for runtime validation
import { z } from 'zod';
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().min(13).max(150),
});
app.post('/users', (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
// result.data is validated and typed
createUser(result.data);
});
```
Never concatenate user input into SQL queries or shell commands.
```javascript
// SQL Injection
const query = `SELECT * FROM users WHERE id = '${req.params.id}'`; // DANGEROUS
// Parameterized query
const query = 'SELECT * FROM users WHERE id = $1';
const result = await pool.query(query, [req.params.id]); // Safe
```
## Authentication and Passwords
### Hash Passwords with bcrypt
```javascript
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
async function hashPassword(password) {
return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password, hash) {
return bcrypt.compare(password, hash);
}
```
Never store plaintext passwords. Never use MD5 or SHA for password hashing. bcrypt and argon2 are the correct choices.
### JWT Best Practices
```javascript
import jwt from 'jsonwebtoken';
const SECRET = process.env.JWT_SECRET; // Never hardcode secrets
function generateToken(user) {
return jwt.sign(
{ sub: user.id, role: user.role },
SECRET,
{ expiresIn: '1h', algorithm: 'HS256' }
);
}
function verifyToken(token) {
return jwt.verify(token, SECRET, { algorithms: ['HS256'] });
}
```
- Always set an expiration
- Always specify the algorithm explicitly
- Store secrets in environment variables, not in code
- Use short-lived access tokens and refresh tokens
## Rate Limiting
Protect against brute-force and denial-of-service attacks.
```javascript
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', limiter);
// Stricter limit for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
});
app.use('/api/auth/login', authLimiter);
```
## Security Headers
Use helmet to set secure HTTP headers.
```javascript
import helmet from 'helmet';
app.use(helmet());
// Sets: X-Content-Type-Options, X-Frame-Options,
// Strict-Transport-Security, Content-Security-Policy, etc.
```
## Dependency Security
Audit your dependencies regularly.
```bash
npm audit
npm audit fix
# Check for known vulnerabilities before installing
npx better-npm-audit audit
```
Pin dependency versions in production. Use lockfiles. Review new dependencies before adding them — check for maintenance activity, known vulnerabilities, and supply chain risks.
## Environment Variables
```javascript
// Never commit .env files
// Use a validation library for env vars
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.coerce.number().default(3000),
});
export const env = envSchema.parse(process.env);
```
Fail fast if required environment variables are missing.
## Error Handling
Do not expose internal error details to clients.
```javascript
app.use((err, req, res, next) => {
console.error(err); // Log internally
if (err.isOperational) {
res.status(err.statusCode).json({ message: err.message });
} else {
res.status(500).json({ message: 'Internal server error' });
}
});
```
## CORS Configuration
```javascript
import cors from 'cors';
app.use(cors({
origin: process.env.ALLOWED_ORIGINS.split(','),
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true,
maxAge: 86400,
}));
```
Never use `origin: '*'` with credentials in production.
## Logging and Monitoring
Log security-relevant events: failed logins, permission changes, payment events. Never log passwords, tokens, or PII.
```javascript
// Good: log what happened without sensitive data
logger.warn('Failed login attempt', { email, ip: req.ip, timestamp: new Date() });
// Bad: logging sensitive data
logger.info(`User ${email} logged in with password ${password}`);
```
## Checklist
- Input validation on every endpoint
- Parameterized database queries
- Passwords hashed with bcrypt/argon2
- JWTs with expiration and algorithm pinning
- Rate limiting on auth and API endpoints
- Security headers via helmet
- Dependencies audited and pinned
- Environment variables validated and secrets stored securely
- Error messages do not leak internals
- CORS configured for specific origins
- Security events logged
Security is never done. Revisit these practices regularly and stay informed about new vulnerabilities.
Continue with related content
Suggested next reads and tools from the knowledge graph.