Backend Security Best Practices
Backend Security Best Practices
Security is paramount in backend development. This comprehensive guide covers essential security practices to protect your applications from common vulnerabilities and attacks.
Authentication & Authorization
JWT Implementation
const jwt = require('jsonwebtoken'); const bcrypt = require('bcrypt'); // Secure password hashing async function hashPassword(password) { const saltRounds = 12; return await bcrypt.hash(password, saltRounds); } // Verify password async function verifyPassword(password, hash) { return await bcrypt.compare(password, hash); } // Generate secure JWT function generateToken(user) { const payload = { id: user.id, email: user.email, role: user.role }; return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '15m', // Short expiration issuer: 'your-app', audience: 'your-app-users' }); } // Generate refresh token function generateRefreshToken(user) { return jwt.sign( { id: user.id, type: 'refresh' }, process.env.REFRESH_SECRET, { expiresIn: '7d' } ); }
Java/Spring Boot:
@Service public class JwtService { @Value("${jwt.secret}") private String secretKey; @Value("${jwt.expiration}") private long jwtExpiration; @Value("${jwt.refresh-expiration}") private long refreshExpiration; // Secure password hashing with BCrypt public String hashPassword(String password) { return BCrypt.hashpw(password, BCrypt.gensalt(12)); } // Verify password public boolean verifyPassword(String password, String hash) { return BCrypt.checkpw(password, hash); } // Generate JWT token public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put("id", ((UserPrincipal) userDetails).getId()); claims.put("email", userDetails.getUsername()); claims.put("role", userDetails.getAuthorities()); return createToken(claims, userDetails.getUsername(), jwtExpiration); } // Generate refresh token public String generateRefreshToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put("id", ((UserPrincipal) userDetails).getId()); claims.put("type", "refresh"); return createToken(claims, userDetails.getUsername(), refreshExpiration); } private String createToken(Map<String, Object> claims, String subject, long expiration) { return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + expiration)) .setIssuer("your-app") .setAudience("your-app-users") .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); } // Validate token public boolean validateToken(String token, UserDetails userDetails) { try { final String username = extractUsername(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } catch (Exception e) { return false; } } private String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } private Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } private Claims extractAllClaims(String token) { return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody(); } private Boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } }
Role-Based Access Control (RBAC)
// Define roles and permissions const ROLES = { ADMIN: 'admin', USER: 'user', MODERATOR: 'moderator' }; const PERMISSIONS = { READ_USERS: 'read:users', WRITE_USERS: 'write:users', DELETE_USERS: 'delete:users', READ_ORDERS: 'read:orders', WRITE_ORDERS: 'write:orders' }; const ROLE_PERMISSIONS = { [ROLES.ADMIN]: [ PERMISSIONS.READ_USERS, PERMISSIONS.WRITE_USERS, PERMISSIONS.DELETE_USERS, PERMISSIONS.READ_ORDERS, PERMISSIONS.WRITE_ORDERS ], [ROLES.USER]: [ PERMISSIONS.READ_ORDERS, PERMISSIONS.WRITE_ORDERS ] }; // Middleware to check permissions function requirePermission(permission) { return (req, res, next) => { const userRole = req.user.role; const userPermissions = ROLE_PERMISSIONS[userRole] || []; if (!userPermissions.includes(permission)) { return res.status(403).json({ error: 'Insufficient permissions' }); } next(); }; } // Usage app.get('/api/users', authenticateToken, requirePermission(PERMISSIONS.READ_USERS), getUsers );
Input Validation & Sanitization
Express Validator
const { body, param, query, validationResult } = require('express-validator'); const { sanitize } = require('express-validator'); // User registration validation const validateUserRegistration = [ body('email') .isEmail() .normalizeEmail() .withMessage('Valid email is required'), body('password') .isLength({ min: 8 }) .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/) .withMessage('Password must be at least 8 characters with uppercase, lowercase, number and special character'), body('name') .trim() .isLength({ min: 2, max: 50 }) .matches(/^[a-zA-Z\s]+$/) .withMessage('Name must be 2-50 characters, letters only'), body('age') .optional() .isInt({ min: 13, max: 120 }) .withMessage('Age must be between 13 and 120'), // Sanitize inputs sanitize('email').trim().toLowerCase(), sanitize('name').trim(), // Handle validation errors (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ error: 'Validation failed', details: errors.array() }); } next(); } ];
SQL Injection Prevention
// Use parameterized queries async function getUserById(id) { // Good: Parameterized query const query = 'SELECT * FROM users WHERE id = $1'; const result = await pool.query(query, [id]); return result.rows[0]; } // Bad: String concatenation (vulnerable to SQL injection) // const query = `SELECT * FROM users WHERE id = ${id}`; // For dynamic queries, use query builder const { Pool } = require('pg'); const { QueryBuilder } = require('knex'); const db = require('knex')({ client: 'postgresql', connection: process.env.DATABASE_URL }); // Safe dynamic query building async function searchUsers(filters) { let query = db('users').select('*'); if (filters.name) { query = query.where('name', 'ilike', `%${filters.name}%`); } if (filters.email) { query = query.where('email', 'ilike', `%${filters.email}%`); } if (filters.status) { query = query.where('status', filters.status); } return await query; }
Rate Limiting & DDoS Protection
Express Rate Limiting
const rateLimit = require('express-rate-limit'); const slowDown = require('express-slow-down'); // General rate limiting const generalLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs message: { error: 'Too many requests from this IP, please try again later.' }, standardHeaders: true, legacyHeaders: false }); // Strict rate limiting for auth endpoints const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // limit each IP to 5 requests per windowMs skipSuccessfulRequests: true, message: { error: 'Too many authentication attempts, please try again later.' } }); // Slow down repeated requests const speedLimiter = slowDown({ windowMs: 15 * 60 * 1000, // 15 minutes delayAfter: 2, // allow 2 requests per 15 minutes, then... delayMs: 500 // begin adding 500ms of delay per request above 2 }); // Apply rate limiting app.use('/api/', generalLimiter); app.use('/api/auth/', authLimiter); app.use('/api/', speedLimiter);
Advanced Rate Limiting
const RedisStore = require('rate-limit-redis'); const redis = require('redis'); const client = redis.createClient({ host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }); // Redis-based rate limiting for distributed systems const redisLimiter = rateLimit({ store: new RedisStore({ sendCommand: (...args) => client.sendCommand(args) }), windowMs: 15 * 60 * 1000, max: 100, keyGenerator: (req) => { // Custom key generation return `${req.ip}:${req.user?.id || 'anonymous'}`; } });
Data Encryption
Sensitive Data Encryption
const crypto = require('crypto'); class EncryptionService { constructor() { this.algorithm = 'aes-256-gcm'; this.key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex'); } encrypt(text) { const iv = crypto.randomBytes(16); const cipher = crypto.createCipher(this.algorithm, this.key); cipher.setAAD(Buffer.from('additional-data')); 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') }; } decrypt(encryptedData) { const decipher = crypto.createDecipher( this.algorithm, this.key ); decipher.setAAD(Buffer.from('additional-data')); decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex')); let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } } // Usage const encryptionService = new EncryptionService(); // Encrypt sensitive data before storing const encryptedSSN = encryptionService.encrypt(user.ssn); await db.query( 'INSERT INTO users (ssn_encrypted, ssn_iv, ssn_auth_tag) VALUES ($1, $2, $3)', [encryptedSSN.encrypted, encryptedSSN.iv, encryptedSSN.authTag] );
Security Headers
Helmet.js Configuration
const helmet = require('helmet'); app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'"], fontSrc: ["'self'"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"], }, }, crossOriginEmbedderPolicy: false, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true } })); // Additional security headers app.use((req, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-XSS-Protection', '1; mode=block'); res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()'); next(); });
CORS Configuration
const cors = require('cors'); const corsOptions = { origin: function (origin, callback) { // Allow requests with no origin (mobile apps, Postman, etc.) if (!origin) return callback(null, true); const allowedOrigins = [ 'https://yourdomain.com', 'https://www.yourdomain.com', 'https://staging.yourdomain.com' ]; if (allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }, credentials: true, optionsSuccessStatus: 200, methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'] }; app.use(cors(corsOptions));
Logging & Monitoring
Security Event Logging
const winston = require('winston'); const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'security.log' }), new winston.transports.Console() ] }); // Log security events function logSecurityEvent(event, details) { logger.warn('Security Event', { event, details, timestamp: new Date().toISOString(), ip: details.ip, userAgent: details.userAgent }); } // Usage app.post('/api/auth/login', async (req, res) => { try { const { email, password } = req.body; const user = await findUserByEmail(email); if (!user) { logSecurityEvent('LOGIN_FAILED', { email, ip: req.ip, userAgent: req.get('User-Agent') }); return res.status(401).json({ error: 'Invalid credentials' }); } // ... rest of login logic } catch (error) { logSecurityEvent('LOGIN_ERROR', { error: error.message, ip: req.ip }); res.status(500).json({ error: 'Internal server error' }); } });
Environment Security
Secure Environment Configuration
// .env.example /* NODE_ENV=production PORT=3000 # Database DATABASE_URL=postgresql://user:password@localhost:5432/dbname # JWT JWT_SECRET=your-super-secret-jwt-key-here REFRESH_SECRET=your-super-secret-refresh-key-here # Encryption ENCRYPTION_KEY=your-32-character-hex-encryption-key # Redis REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD=your-redis-password # External APIs API_KEY=your-api-key API_SECRET=your-api-secret */ // Validate required environment variables const requiredEnvVars = [ 'NODE_ENV', 'DATABASE_URL', 'JWT_SECRET', 'REFRESH_SECRET', 'ENCRYPTION_KEY' ]; function validateEnvironment() { const missing = requiredEnvVars.filter(envVar => !process.env[envVar]); if (missing.length > 0) { throw new Error(`Missing required environment variables: ${missing.join(', ')}`); } // Validate JWT secret strength if (process.env.JWT_SECRET.length < 32) { throw new Error('JWT_SECRET must be at least 32 characters long'); } } validateEnvironment();
API Security Testing
Automated Security Tests
const request = require('supertest'); const app = require('../app'); describe('Security Tests', () => { test('should reject requests without authentication', async () => { const response = await request(app) .get('/api/users') .expect(401); expect(response.body.error).toBe('Access token required'); }); test('should reject invalid JWT tokens', async () => { const response = await request(app) .get('/api/users') .set('Authorization', 'Bearer invalid-token') .expect(403); expect(response.body.error).toBe('Invalid token'); }); test('should prevent SQL injection', async () => { const maliciousInput = "'; DROP TABLE users; --"; const response = await request(app) .get(`/api/users?search=${maliciousInput}`) .set('Authorization', 'Bearer valid-token') .expect(200); // Should not crash or return unexpected data expect(response.body.success).toBe(true); }); test('should enforce rate limiting', async () => { const promises = []; // Make 10 requests quickly for (let i = 0; i < 10; i++) { promises.push( request(app) .post('/api/auth/login') .send({ email: 'test@example.com', password: 'wrong' }) ); } const responses = await Promise.all(promises); // Some requests should be rate limited const rateLimitedResponses = responses.filter( res => res.status === 429 ); expect(rateLimitedResponses.length).toBeGreaterThan(0); }); });
Conclusion
Security is not a one-time implementation but an ongoing process. Key takeaways:
- Always validate and sanitize input
- Use parameterized queries to prevent SQL injection
- Implement proper authentication and authorization
- Use HTTPS everywhere
- Set security headers
- Implement rate limiting
- Log security events
- Keep dependencies updated
- Regular security audits
- Follow the principle of least privilege
Remember: Security is everyone's responsibility, and it's better to be proactive than reactive when it comes to protecting your application and users' data.
Related Articles
Incident Playbook for Beginners: Real-World Monitoring and Troubleshooting Stories
A story-driven, plain English incident playbook for new backend & SRE engineers. Find, fix, and prevent outages with empathy and practical steps.
System Design Power-Guide 2025: What To Learn, In What Order, With Real-World Links
Stop bookmarking random threads. This is a tight, no-fluff map of what to study for system design in 2025 - what each topic is, why it matters in interviews and production, and where to go deeper.
DSA Patterns Master Guide: How To Identify Problems, Pick Patterns, and Practice (With LeetCode Sets)
A practical, pattern-first road map for entry-level engineers. Learn how to identify the right pattern quickly, apply a small algorithm template, know variants and pitfalls, and practice with curated LeetCode problems.