Web Security Essentials: Protecting Your Applications
Web security isn’t optional—it’s essential. Let’s explore the critical security measures every developer needs to implement to protect applications and user data from modern threats.
Authentication and Authorization
Secure Password Handling
const bcrypt = require('bcrypt');
const crypto = require('crypto');
class PasswordService {
static async hashPassword(password) {
// Validate password strength
if (!this.isStrongPassword(password)) {
throw new Error('Password does not meet security requirements');
}
const saltRounds = 12; // Adjust based on security needs vs performance
return await bcrypt.hash(password, saltRounds);
}
static async verifyPassword(password, hashedPassword) {
return await bcrypt.compare(password, hashedPassword);
}
static isStrongPassword(password) {
// At least 8 characters, 1 uppercase, 1 lowercase, 1 number, 1 special char
const strongPasswordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
return strongPasswordRegex.test(password);
}
static generateSecureToken(length = 32) {
return crypto.randomBytes(length).toString('hex');
}
}
// Usage in user registration
app.post('/register', async (req, res) => {
try {
const { email, password } = req.body;
// Check if user exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: 'User already exists' });
}
// Hash password
const hashedPassword = await PasswordService.hashPassword(password);
// Create user
const user = await User.create({
email,
password: hashedPassword,
emailVerificationToken: PasswordService.generateSecureToken()
});
// Send verification email
await sendVerificationEmail(user.email, user.emailVerificationToken);
res.status(201).json({ message: 'User created. Please verify your email.' });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
JWT Security Best Practices
const jwt = require('jsonwebtoken');
const redis = require('redis');
const client = redis.createClient();
class JWTService {
static generateTokens(payload) {
const accessToken = jwt.sign(
payload,
process.env.JWT_ACCESS_SECRET,
{
expiresIn: '15m',
issuer: 'your-app',
audience: 'your-app-users'
}
);
const refreshToken = jwt.sign(
{ userId: payload.userId },
process.env.JWT_REFRESH_SECRET,
{
expiresIn: '7d',
issuer: 'your-app',
audience: 'your-app-users'
}
);
return { accessToken, refreshToken };
}
static async verifyAccessToken(token) {
try {
// Check if token is blacklisted
const isBlacklisted = await client.get(`blacklist:${token}`);
if (isBlacklisted) {
throw new Error('Token is blacklisted');
}
return jwt.verify(token, process.env.JWT_ACCESS_SECRET, {
issuer: 'your-app',
audience: 'your-app-users'
});
} catch (error) {
throw new Error('Invalid or expired token');
}
}
static async blacklistToken(token) {
const decoded = jwt.decode(token);
const expiresIn = decoded.exp - Math.floor(Date.now() / 1000);
if (expiresIn > 0) {
await client.setex(`blacklist:${token}`, expiresIn, 'true');
}
}
static async refreshTokens(refreshToken) {
try {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
// Check if refresh token is still valid in database
const user = await User.findById(decoded.userId);
if (!user || user.refreshTokenVersion !== decoded.tokenVersion) {
throw new Error('Invalid refresh token');
}
return this.generateTokens({
userId: user.id,
email: user.email,
role: user.role
});
} catch (error) {
throw new Error('Invalid refresh token');
}
}
}
// Secure authentication middleware
const authenticateToken = async (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
try {
const decoded = await JWTService.verifyAccessToken(token);
req.user = decoded;
next();
} catch (error) {
res.status(403).json({ error: error.message });
}
};
Input Validation and Sanitization
Comprehensive Input Validation
const validator = require('validator');
const DOMPurify = require('isomorphic-dompurify');
class InputValidator {
static sanitizeString(input, maxLength = 255) {
if (typeof input !== 'string') {
throw new Error('Input must be a string');
}
// Remove null bytes and control characters
let sanitized = input.replace(/[\x00-\x1F\x7F]/g, '');
// Trim whitespace
sanitized = sanitized.trim();
// Limit length
if (sanitized.length > maxLength) {
throw new Error(`Input exceeds maximum length of ${maxLength}`);
}
return sanitized;
}
static sanitizeHTML(input) {
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
ALLOWED_ATTR: []
});
}
static validateEmail(email) {
if (!validator.isEmail(email)) {
throw new Error('Invalid email format');
}
// Additional checks
if (email.length > 254) {
throw new Error('Email too long');
}
return validator.normalizeEmail(email);
}
static validateURL(url) {
if (!validator.isURL(url, {
protocols: ['http', 'https'],
require_protocol: true,
require_valid_protocol: true
})) {
throw new Error('Invalid URL format');
}
return url;
}
static validateInteger(value, min = null, max = null) {
const num = parseInt(value, 10);
if (isNaN(num)) {
throw new Error('Value must be an integer');
}
if (min !== null && num < min) {
throw new Error(`Value must be at least ${min}`);
}
if (max !== null && num > max) {
throw new Error(`Value must be at most ${max}`);
}
return num;
}
}
// SQL Injection Prevention
const db = require('pg');
const pool = new db.Pool(/* config */);
// BAD: Vulnerable to SQL injection
const getUserBad = async (userId) => {
const query = `SELECT * FROM users WHERE id = ${userId}`;
return await pool.query(query);
};
// GOOD: Using parameterized queries
const getUserGood = async (userId) => {
const query = 'SELECT * FROM users WHERE id = $1';
return await pool.query(query, [userId]);
};
// NoSQL Injection Prevention (MongoDB)
const getUserMongo = async (userId) => {
// Validate input type
if (typeof userId !== 'string') {
throw new Error('Invalid user ID format');
}
// Use strict matching
return await User.findOne({
_id: mongoose.Types.ObjectId(userId)
});
};
Cross-Site Scripting (XSS) Prevention
Content Security Policy
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
styleSrc: [
"'self'",
"'unsafe-inline'", // Only if absolutely necessary
"https://fonts.googleapis.com"
],
scriptSrc: [
"'self'",
"https://apis.google.com"
],
imgSrc: [
"'self'",
"data:",
"https:"
],
connectSrc: [
"'self'",
"https://api.example.com"
],
fontSrc: [
"'self'",
"https://fonts.gstatic.com"
],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
upgradeInsecureRequests: []
}
}));
// XSS Protection Headers
app.use(helmet.xssFilter());
app.use(helmet.noSniff());
app.use(helmet.frameguard({ action: 'deny' }));
Output Encoding
const he = require('he');
class OutputEncoder {
static encodeHTML(input) {
return he.encode(input, {
useNamedReferences: true,
decimal: false
});
}
static encodeHTMLAttribute(input) {
return he.encode(input, {
useNamedReferences: true,
decimal: false,
encodeEverything: true
});
}
static encodeURL(input) {
return encodeURIComponent(input);
}
static encodeJS(input) {
return JSON.stringify(input).slice(1, -1); // Remove quotes
}
}
// Template rendering with encoding
app.get('/profile', authenticateToken, async (req, res) => {
const user = await User.findById(req.user.userId);
res.render('profile', {
userName: OutputEncoder.encodeHTML(user.name),
userBio: OutputEncoder.encodeHTML(user.bio),
avatarUrl: OutputEncoder.encodeHTMLAttribute(user.avatarUrl)
});
});
Cross-Site Request Forgery (CSRF) Protection
CSRF Token Implementation
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
// Configure CSRF protection
const csrfProtection = csrf({
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
}
});
app.use(cookieParser());
app.use(csrfProtection);
// Provide CSRF token to frontend
app.get('/csrf-token', (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// Double Submit Cookie Pattern (alternative)
class CSRFService {
static generateToken() {
return crypto.randomBytes(32).toString('hex');
}
static setCSRFCookie(res, token) {
res.cookie('csrf-token', token, {
httpOnly: false, // Accessible to JavaScript
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 3600000 // 1 hour
});
}
static validateCSRF(req) {
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
throw new Error('CSRF token validation failed');
}
}
}
Rate Limiting and DDoS Protection
Advanced Rate Limiting
const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');
const RedisStore = require('rate-limit-redis');
const redis = require('redis');
const redisClient = redis.createClient();
// Different rate limits for different endpoints
const createRateLimiter = (windowMs, max, message, skipSuccessfulRequests = false) => {
return rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:'
}),
windowMs,
max,
message: { error: message },
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests,
keyGenerator: (req) => {
// Use IP + User ID for authenticated requests
return req.user ? `${req.ip}:${req.user.userId}` : req.ip;
}
});
};
// Progressive delay for repeated requests
const speedLimiter = slowDown({
store: new RedisStore({
client: redisClient,
prefix: 'sl:'
}),
windowMs: 15 * 60 * 1000, // 15 minutes
delayAfter: 5, // Allow 5 requests per windowMs without delay
delayMs: 500, // Add 500ms delay per request after delayAfter
maxDelayMs: 20000 // Maximum delay of 20 seconds
});
// Apply different limits
app.use('/api/auth/login', createRateLimiter(15 * 60 * 1000, 5, 'Too many login attempts'));
app.use('/api/auth/register', createRateLimiter(60 * 60 * 1000, 3, 'Too many registration attempts'));
app.use('/api/', speedLimiter);
app.use('/api/', createRateLimiter(15 * 60 * 1000, 100, 'Too many requests'));
// IP-based blocking for suspicious activity
class SecurityMonitor {
constructor() {
this.suspiciousIPs = new Map();
this.blockedIPs = new Set();
}
recordSuspiciousActivity(ip, activity) {
const current = this.suspiciousIPs.get(ip) || { count: 0, activities: [] };
current.count++;
current.activities.push({ activity, timestamp: Date.now() });
// Keep only recent activities (last hour)
current.activities = current.activities.filter(
a => Date.now() - a.timestamp < 3600000
);
this.suspiciousIPs.set(ip, current);
// Block IP if too many suspicious activities
if (current.count > 10) {
this.blockedIPs.add(ip);
console.warn(`Blocked suspicious IP: ${ip}`);
}
}
isBlocked(ip) {
return this.blockedIPs.has(ip);
}
middleware() {
return (req, res, next) => {
if (this.isBlocked(req.ip)) {
return res.status(403).json({ error: 'Access denied' });
}
next();
};
}
}
const securityMonitor = new SecurityMonitor();
app.use(securityMonitor.middleware());
Secure Headers and HTTPS
Security Headers Implementation
const helmet = require('helmet');
app.use(helmet({
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"]
}
},
// HTTP Strict Transport Security
hsts: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true
},
// Prevent clickjacking
frameguard: { action: 'deny' },
// Prevent MIME type sniffing
noSniff: true,
// XSS Protection
xssFilter: true,
// Referrer Policy
referrerPolicy: { policy: 'same-origin' },
// Feature Policy
featurePolicy: {
features: {
camera: ["'none'"],
microphone: ["'none'"],
geolocation: ["'self'"]
}
}
}));
// HTTPS Redirect
app.use((req, res, next) => {
if (process.env.NODE_ENV === 'production' && !req.secure && req.get('x-forwarded-proto') !== 'https') {
return res.redirect(301, `https://${req.get('host')}${req.url}`);
}
next();
});
Security Monitoring and Logging
Security Event Logging
const winston = require('winston');
const securityLogger = 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()
]
});
class SecurityLogger {
static logAuthAttempt(ip, email, success, reason = null) {
securityLogger.info('Authentication attempt', {
type: 'auth_attempt',
ip,
email,
success,
reason,
timestamp: new Date().toISOString()
});
}
static logSuspiciousActivity(ip, activity, details = {}) {
securityLogger.warn('Suspicious activity detected', {
type: 'suspicious_activity',
ip,
activity,
details,
timestamp: new Date().toISOString()
});
}
static logSecurityEvent(event, details = {}) {
securityLogger.error('Security event', {
type: 'security_event',
event,
details,
timestamp: new Date().toISOString()
});
}
}
// Usage in authentication
app.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !await PasswordService.verifyPassword(password, user.password)) {
SecurityLogger.logAuthAttempt(req.ip, email, false, 'Invalid credentials');
securityMonitor.recordSuspiciousActivity(req.ip, 'failed_login');
return res.status(401).json({ error: 'Invalid credentials' });
}
SecurityLogger.logAuthAttempt(req.ip, email, true);
const tokens = JWTService.generateTokens({
userId: user.id,
email: user.email,
role: user.role
});
res.json(tokens);
} catch (error) {
SecurityLogger.logSecurityEvent('login_error', { error: error.message, ip: req.ip });
res.status(500).json({ error: 'Internal server error' });
}
});
Web security is an ongoing process, not a one-time implementation. Stay updated with the latest security threats, regularly audit your code, and always follow the principle of least privilege. Remember: security is only as strong as its weakest link.