Web Security Best Practices
Implement comprehensive web security measures including authentication, authorization, and protection against common vulnerabilities
# Web Security Best Practices
## 1. Authentication and Authorization
### Secure Authentication Implementation
```javascript
// Secure password hashing with bcrypt
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
class AuthService {
constructor() {
this.saltRounds = 12;
this.jwtSecret = process.env.JWT_SECRET;
this.jwtExpiry = '15m';
this.refreshTokenExpiry = '7d';
}
async hashPassword(password) {
// Validate password strength
if (!this.isPasswordSecure(password)) {
throw new Error('Password does not meet security requirements');
}
return await bcrypt.hash(password, this.saltRounds);
}
async verifyPassword(password, hashedPassword) {
return await bcrypt.compare(password, hashedPassword);
}
isPasswordSecure(password) {
// Minimum 12 characters, at least one uppercase, lowercase, number, and special character
const minLength = 12;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(password);
return password.length >= minLength && hasUpperCase && hasLowerCase && hasNumbers && hasSpecial;
}
generateTokens(userId, userRole) {
const payload = {
userId,
role: userRole,
type: 'access'
};
const accessToken = jwt.sign(payload, this.jwtSecret, {
expiresIn: this.jwtExpiry,
issuer: 'your-app',
audience: 'your-app-users'
});
const refreshPayload = {
userId,
type: 'refresh',
jti: crypto.randomBytes(16).toString('hex') // JWT ID for token revocation
};
const refreshToken = jwt.sign(refreshPayload, this.jwtSecret, {
expiresIn: this.refreshTokenExpiry,
issuer: 'your-app',
audience: 'your-app-users'
});
return { accessToken, refreshToken };
}
verifyToken(token, expectedType = 'access') {
try {
const decoded = jwt.verify(token, this.jwtSecret, {
issuer: 'your-app',
audience: 'your-app-users'
});
if (decoded.type !== expectedType) {
throw new Error('Invalid token type');
}
return decoded;
} catch (error) {
throw new Error('Invalid or expired token');
}
}
// Rate limiting for authentication attempts
async checkAuthAttempts(identifier) {
const key = `auth_attempts:${identifier}`;
const attempts = await redis.get(key) || 0;
if (attempts >= 5) {
const ttl = await redis.ttl(key);
throw new Error(`Too many authentication attempts. Try again in ${ttl} seconds`);
}
return true;
}
async recordAuthAttempt(identifier, success) {
const key = `auth_attempts:${identifier}`;
if (success) {
await redis.del(key);
} else {
await redis.incr(key);
await redis.expire(key, 900); // 15 minutes lockout
}
}
}
```
### Multi-Factor Authentication (MFA)
```javascript
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
class MFAService {
generateSecret(userEmail) {
const secret = speakeasy.generateSecret({
name: userEmail,
issuer: 'Your App Name',
length: 32
});
return {
secret: secret.base32,
qrCodeUrl: secret.otpauth_url,
backupCodes: this.generateBackupCodes()
};
}
async generateQRCode(otpauthUrl) {
return await QRCode.toDataURL(otpauthUrl);
}
verifyToken(secret, token) {
return speakeasy.totp.verify({
secret,
encoding: 'base32',
token,
window: 2 // Allow 2 time steps of tolerance
});
}
generateBackupCodes() {
const codes = [];
for (let i = 0; i < 10; i++) {
codes.push(crypto.randomBytes(4).toString('hex').toUpperCase());
}
return codes;
}
async validateBackupCode(userId, code) {
// Retrieve user's backup codes from database
const user = await User.findById(userId);
const codeIndex = user.backupCodes.indexOf(code);
if (codeIndex === -1) {
return false;
}
// Remove used backup code
user.backupCodes.splice(codeIndex, 1);
await user.save();
return true;
}
}
```
## 2. Input Validation and Sanitization
### Comprehensive Input Validation
```javascript
const validator = require('validator');
const DOMPurify = require('isomorphic-dompurify');
class InputValidator {
static validateEmail(email) {
if (!email || typeof email !== 'string') {
throw new Error('Email is required and must be a string');
}
if (!validator.isEmail(email)) {
throw new Error('Invalid email format');
}
if (email.length > 254) {
throw new Error('Email address too long');
}
return email.toLowerCase().trim();
}
static validateUrl(url, options = {}) {
const defaultOptions = {
require_protocol: true,
require_host: true,
require_valid_protocol: true,
allow_underscores: false,
host_whitelist: false,
host_blacklist: false,
allow_trailing_dot: false,
allow_protocol_relative_urls: false
};
const validationOptions = { ...defaultOptions, ...options };
if (!validator.isURL(url, validationOptions)) {
throw new Error('Invalid URL format');
}
return url;
}
static sanitizeHtml(input) {
if (typeof input !== 'string') {
throw new Error('Input must be a string');
}
// Configure DOMPurify to allow only safe tags and attributes
const config = {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'title'],
ALLOW_DATA_ATTR: false,
ALLOW_UNKNOWN_PROTOCOLS: false
};
return DOMPurify.sanitize(input, config);
}
static validateAndSanitizeInput(input, rules) {
const errors = [];
let sanitizedInput = input;
// Required field validation
if (rules.required && (!input || input.toString().trim().length === 0)) {
errors.push('This field is required');
}
if (input) {
// Type validation
if (rules.type === 'email') {
try {
sanitizedInput = this.validateEmail(input);
} catch (error) {
errors.push(error.message);
}
}
if (rules.type === 'url') {
try {
sanitizedInput = this.validateUrl(input, rules.urlOptions);
} catch (error) {
errors.push(error.message);
}
}
// Length validation
if (rules.minLength && input.length < rules.minLength) {
errors.push(`Minimum length is ${rules.minLength} characters`);
}
if (rules.maxLength && input.length > rules.maxLength) {
errors.push(`Maximum length is ${rules.maxLength} characters`);
}
// Pattern validation
if (rules.pattern && !rules.pattern.test(input)) {
errors.push(rules.patternMessage || 'Invalid format');
}
// Custom validation function
if (rules.customValidator) {
try {
sanitizedInput = rules.customValidator(input);
} catch (error) {
errors.push(error.message);
}
}
// Sanitization
if (rules.sanitize === 'html') {
sanitizedInput = this.sanitizeHtml(sanitizedInput);
}
if (rules.sanitize === 'trim') {
sanitizedInput = sanitizedInput.toString().trim();
}
}
return {
isValid: errors.length === 0,
errors,
sanitizedValue: sanitizedInput
};
}
// SQL injection prevention
static preventSQLInjection(query, params) {
// Use parameterized queries with prepared statements
// Example with mysql2
return db.execute(query, params);
}
// NoSQL injection prevention
static sanitizeMongoQuery(query) {
if (typeof query !== 'object' || query === null) {
return query;
}
const sanitized = {};
for (const [key, value] of Object.entries(query)) {
// Prevent operator injection
if (key.startsWith('$')) {
continue;
}
if (typeof value === 'object' && value !== null) {
sanitized[key] = this.sanitizeMongoQuery(value);
} else {
sanitized[key] = value;
}
}
return sanitized;
}
}
```
## 3. HTTPS and Transport Security
### SSL/TLS Configuration
```javascript
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const app = express();
// Security headers with Helmet
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", "trusted-cdn.com"],
styleSrc: ["'self'", "'unsafe-inline'", "trusted-cdn.com"],
imgSrc: ["'self'", "data:", "trusted-cdn.com"],
connectSrc: ["'self'"],
fontSrc: ["'self'", "trusted-cdn.com"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
upgradeInsecureRequests: []
}
},
hsts: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true
},
noSniff: true,
frameguard: { action: 'deny' },
xssFilter: true
}));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later.',
standardHeaders: true,
legacyHeaders: false
});
app.use('/api/', limiter);
// Strict rate limiting for authentication endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true,
message: 'Too many authentication attempts, please try again later.'
});
app.use('/api/auth/', authLimiter);
// HTTPS redirect middleware
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
res.redirect(`https://${req.header('host')}${req.url}`);
} else {
next();
}
});
```
### Certificate Pinning and HSTS
```javascript
// Certificate pinning middleware
const crypto = require('crypto');
function certificatePinning(expectedFingerprints) {
return (req, res, next) => {
const cert = req.connection.getPeerCertificate();
if (!cert || !cert.raw) {
return res.status(400).json({ error: 'No certificate provided' });
}
const fingerprint = crypto
.createHash('sha256')
.update(cert.raw)
.digest('hex');
if (!expectedFingerprints.includes(fingerprint)) {
return res.status(400).json({ error: 'Certificate validation failed' });
}
next();
};
}
// Use certificate pinning for critical endpoints
app.use('/api/admin/', certificatePinning([
'expected-certificate-fingerprint-1',
'expected-certificate-fingerprint-2'
]));
```
## 4. Cross-Site Scripting (XSS) Prevention
### XSS Protection Strategies
```javascript
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
class XSSProtection {
static sanitizeUserInput(input, options = {}) {
const defaultConfig = {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li', 'h1', 'h2', 'h3'],
ALLOWED_ATTR: ['href', 'title', 'alt'],
ALLOW_DATA_ATTR: false,
ALLOW_UNKNOWN_PROTOCOLS: false,
RETURN_TRUSTED_TYPE: true
};
const config = { ...defaultConfig, ...options };
return DOMPurify.sanitize(input, config);
}
static escapeHtml(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
static validateCSP(req, res, next) {
// Generate nonce for inline scripts
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
// Set CSP header with nonce
res.setHeader('Content-Security-Policy',
`default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; object-src 'none';`
);
next();
}
// Template rendering with automatic escaping
static renderTemplate(templateString, data) {
// Escape all variables by default
const escapedData = {};
for (const [key, value] of Object.entries(data)) {
if (typeof value === 'string') {
escapedData[key] = this.escapeHtml(value);
} else {
escapedData[key] = value;
}
}
return templateString.replace(/{{\s*(.*?)\s*}}/g, (match, variable) => {
return escapedData[variable.trim()] || '';
});
}
}
// Express middleware for XSS protection
app.use((req, res, next) => {
// Sanitize query parameters
for (const [key, value] of Object.entries(req.query)) {
if (typeof value === 'string') {
req.query[key] = XSSProtection.sanitizeUserInput(value);
}
}
// Sanitize body parameters
if (req.body && typeof req.body === 'object') {
for (const [key, value] of Object.entries(req.body)) {
if (typeof value === 'string') {
req.body[key] = XSSProtection.sanitizeUserInput(value);
}
}
}
next();
});
```
## 5. Cross-Site Request Forgery (CSRF) Protection
### CSRF Token Implementation
```javascript
const csrf = require('csurf');
// CSRF protection middleware
const csrfProtection = csrf({
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
}
});
app.use(csrfProtection);
// Provide CSRF token to templates
app.use((req, res, next) => {
res.locals.csrfToken = req.csrfToken();
next();
});
// Custom CSRF implementation
class CSRFProtection {
static generateToken() {
return crypto.randomBytes(32).toString('hex');
}
static validateToken(sessionToken, requestToken) {
if (!sessionToken || !requestToken) {
return false;
}
return crypto.timingSafeEqual(
Buffer.from(sessionToken, 'hex'),
Buffer.from(requestToken, 'hex')
);
}
static middleware(req, res, next) {
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
return next();
}
const sessionToken = req.session.csrfToken;
const requestToken = req.body._csrf || req.headers['x-csrf-token'];
if (!this.validateToken(sessionToken, requestToken)) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
next();
}
}
// Apply CSRF protection to state-changing operations
app.use('/api/', CSRFProtection.middleware);
```
## 6. Secure Session Management
### Session Security Implementation
```javascript
const session = require('express-session');
const MongoStore = require('connect-mongo');
// Secure session configuration
app.use(session({
secret: process.env.SESSION_SECRET,
name: 'sessionId', // Don't use default name
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
httpOnly: true, // Prevent XSS access to cookies
maxAge: 1000 * 60 * 30, // 30 minutes
sameSite: 'strict' // CSRF protection
},
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI,
touchAfter: 24 * 3600 // lazy session update
})
}));
class SessionManager {
static regenerateSession(req) {
return new Promise((resolve, reject) => {
req.session.regenerate((err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
static destroySession(req) {
return new Promise((resolve, reject) => {
req.session.destroy((err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
static async secureLogin(req, user) {
// Regenerate session ID to prevent session fixation
await this.regenerateSession(req);
// Store minimal user information in session
req.session.userId = user.id;
req.session.userRole = user.role;
req.session.loginTime = new Date();
// Track session for security monitoring
await this.trackSession(req.session.id, user.id, req.ip);
}
static async trackSession(sessionId, userId, ip) {
await SessionLog.create({
sessionId,
userId,
ip,
userAgent: req.headers['user-agent'],
createdAt: new Date()
});
}
static async validateSession(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: 'Authentication required' });
}
// Check session age
const sessionAge = Date.now() - new Date(req.session.loginTime).getTime();
const maxSessionAge = 1000 * 60 * 60 * 8; // 8 hours
if (sessionAge > maxSessionAge) {
await this.destroySession(req);
return res.status(401).json({ error: 'Session expired' });
}
// Check for suspicious activity
const suspiciousActivity = await this.checkSuspiciousActivity(req.session.userId, req.ip);
if (suspiciousActivity) {
await this.destroySession(req);
return res.status(401).json({ error: 'Suspicious activity detected' });
}
next();
}
static async checkSuspiciousActivity(userId, currentIp) {
// Check for multiple concurrent sessions from different IPs
const recentSessions = await SessionLog.find({
userId,
createdAt: { $gte: new Date(Date.now() - 1000 * 60 * 60) } // Last hour
});
const uniqueIPs = [...new Set(recentSessions.map(s => s.ip))];
return uniqueIPs.length > 3; // More than 3 different IPs in an hour
}
}
```
## 7. File Upload Security
### Secure File Upload Implementation
```javascript
const multer = require('multer');
const path = require('path');
const crypto = require('crypto');
class SecureFileUpload {
constructor() {
this.allowedMimeTypes = [
'image/jpeg',
'image/png',
'image/gif',
'application/pdf',
'text/plain'
];
this.maxFileSize = 5 * 1024 * 1024; // 5MB
this.uploadDir = path.join(__dirname, '../uploads');
}
createUploadMiddleware() {
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, this.uploadDir);
},
filename: (req, file, cb) => {
// Generate secure random filename
const uniqueName = crypto.randomBytes(16).toString('hex');
const extension = path.extname(file.originalname);
cb(null, uniqueName + extension);
}
});
return multer({
storage,
limits: {
fileSize: this.maxFileSize,
files: 5 // Maximum 5 files
},
fileFilter: this.fileFilter.bind(this)
});
}
fileFilter(req, file, cb) {
// Check MIME type
if (!this.allowedMimeTypes.includes(file.mimetype)) {
return cb(new Error('File type not allowed'), false);
}
// Check file extension
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.pdf', '.txt'];
const fileExtension = path.extname(file.originalname).toLowerCase();
if (!allowedExtensions.includes(fileExtension)) {
return cb(new Error('File extension not allowed'), false);
}
// Additional checks can be added here
cb(null, true);
}
async validateFileContent(filePath) {
const fs = require('fs').promises;
try {
// Read file header to verify actual file type
const fileBuffer = await fs.readFile(filePath);
const fileSignature = fileBuffer.toString('hex', 0, 4);
const signatures = {
'89504e47': 'image/png',
'ffd8ffe0': 'image/jpeg',
'ffd8ffe1': 'image/jpeg',
'ffd8ffe2': 'image/jpeg',
'47494638': 'image/gif',
'25504446': 'application/pdf'
};
const detectedType = signatures[fileSignature];
if (!detectedType || !this.allowedMimeTypes.includes(detectedType)) {
await fs.unlink(filePath); // Delete suspicious file
throw new Error('File content does not match extension');
}
return true;
} catch (error) {
throw new Error('File validation failed');
}
}
async scanForMalware(filePath) {
// Implement virus scanning (e.g., using ClamAV)
// This is a placeholder for actual malware scanning
return new Promise((resolve) => {
setTimeout(() => resolve(true), 100); // Simulated scan
});
}
generateSecureUrl(filename) {
const token = crypto.randomBytes(32).toString('hex');
const expiry = Date.now() + (1000 * 60 * 60); // 1 hour
// Store token with expiry in Redis or database
redis.setex(`file_token:${token}`, 3600, JSON.stringify({
filename,
expiry
}));
return `/secure-files/${token}`;
}
}
// Usage example
const fileUpload = new SecureFileUpload();
const upload = fileUpload.createUploadMiddleware();
app.post('/upload', upload.array('files'), async (req, res) => {
try {
for (const file of req.files) {
// Validate file content
await fileUpload.validateFileContent(file.path);
// Scan for malware
const isClean = await fileUpload.scanForMalware(file.path);
if (!isClean) {
await fs.unlink(file.path);
throw new Error('File failed security scan');
}
// Generate secure access URL
const secureUrl = fileUpload.generateSecureUrl(file.filename);
file.secureUrl = secureUrl;
}
res.json({
message: 'Files uploaded successfully',
files: req.files.map(f => ({
originalName: f.originalname,
size: f.size,
secureUrl: f.secureUrl
}))
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
```
## Checklist for Web Security Implementation
- [ ] Implement secure password hashing with bcrypt
- [ ] Set up JWT tokens with proper expiration and rotation
- [ ] Add multi-factor authentication support
- [ ] Implement comprehensive input validation and sanitization
- [ ] Configure HTTPS with proper SSL/TLS settings
- [ ] Set up security headers (CSP, HSTS, etc.)
- [ ] Implement XSS protection with content sanitization
- [ ] Add CSRF protection for state-changing operations
- [ ] Configure secure session management
- [ ] Implement rate limiting and authentication throttling
- [ ] Set up secure file upload with content validation
- [ ] Add logging and monitoring for security events
- [ ] Implement proper error handling without information disclosure
- [ ] Regular security audits and dependency updates
- [ ] Set up automated security testing in CI/CD pipeline