Web Security Best Practices for Modern Applications
Web security is not optional—it's essential. With cyber attacks becoming more sophisticated and data breaches making headlines regularly, implementing robust security measures is crucial for any web application. This comprehensive guide covers the essential security practices every developer should implement.
The Security Landscape
Common Web Vulnerabilities (OWASP Top 10):
- Injection (SQL, NoSQL, OS, LDAP)
- Broken Authentication
- Sensitive Data Exposure
- XML External Entities (XXE)
- Broken Access Control
- Security Misconfiguration
- Cross-Site Scripting (XSS)
- Insecure Deserialization
- Using Components with Known Vulnerabilities
- Insufficient Logging & Monitoring
HTTPS and Transport Security
1. Enforce HTTPS Everywhere
// Express.js middleware to enforce HTTPS
function enforceHTTPS(req, res, next) {
if (!req.secure && req.get('x-forwarded-proto') !== 'https') {
return res.redirect(301, `https://${req.get('host')}${req.url}`);
}
next();
}
app.use(enforceHTTPS);
2. HTTP Strict Transport Security (HSTS)
// Set HSTS header
app.use((req, res, next) => {
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
next();
});
3. Certificate Management
# Using Let's Encrypt with Certbot
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Auto-renewal
sudo crontab -e
# Add: 0 12 * * * /usr/bin/certbot renew --quiet
Content Security Policy (CSP)
CSP helps prevent XSS attacks by controlling which resources can be loaded:
// Basic CSP implementation
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
"font-src 'self' https://fonts.gstatic.com; " +
"img-src 'self' data: https:; " +
"connect-src 'self' https://api.yourdomain.com;"
);
next();
});
Advanced CSP with Nonces
const crypto = require('crypto');
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
res.setHeader(
'Content-Security-Policy',
`script-src 'self' 'nonce-${res.locals.nonce}'; object-src 'none';`
);
next();
});
// In your template
// <script nonce="<%= nonce %>">...</script>
Authentication and Authorization
1. Secure Password Handling
const bcrypt = require('bcrypt');
const saltRounds = 12;
// Hash password before storing
async function hashPassword(password) {
return await bcrypt.hash(password, saltRounds);
}
// Verify password
async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
// Password strength validation
function validatePassword(password) {
const minLength = 8;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
return password.length >= minLength &&
hasUpperCase &&
hasLowerCase &&
hasNumbers &&
hasSpecialChar;
}
2. JWT Security Best Practices
const jwt = require('jsonwebtoken');
// Generate secure JWT
function generateToken(payload) {
return jwt.sign(
payload,
process.env.JWT_SECRET,
{
expiresIn: '15m', // Short expiration
issuer: 'your-app-name',
audience: 'your-app-users'
}
);
}
// Verify JWT with proper error handling
function verifyToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET, {
issuer: 'your-app-name',
audience: 'your-app-users'
});
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('Token expired');
} else if (error.name === 'JsonWebTokenError') {
throw new Error('Invalid token');
}
throw error;
}
}
// Refresh token implementation
function generateRefreshToken(userId) {
return jwt.sign(
{ userId, type: 'refresh' },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);
}
3. Multi-Factor Authentication (MFA)
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// Generate MFA secret
function generateMFASecret(userEmail) {
return speakeasy.generateSecret({
name: userEmail,
service: 'Your App Name',
length: 32
});
}
// Generate QR code for MFA setup
async function generateQRCode(secret) {
return await QRCode.toDataURL(secret.otpauth_url);
}
// Verify MFA token
function verifyMFAToken(token, secret) {
return speakeasy.totp.verify({
secret: secret,
encoding: 'base32',
token: token,
window: 2 // Allow 2 time steps of variance
});
}
Input Validation and Sanitization
1. Server-Side Validation
const Joi = require('joi');
const DOMPurify = require('isomorphic-dompurify');
// Input validation schema
const userSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
name: Joi.string().min(2).max(50).required(),
age: Joi.number().integer().min(13).max(120)
});
// Validation middleware
function validateInput(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).json({
error: 'Validation failed',
details: error.details
});
}
req.validatedData = value;
next();
};
}
// Sanitize HTML input
function sanitizeHTML(dirty) {
return DOMPurify.sanitize(dirty);
}
2. SQL Injection Prevention
// Using parameterized queries with PostgreSQL
const { Pool } = require('pg');
const pool = new Pool();
// Safe query with parameters
async function getUserById(userId) {
const query = 'SELECT * FROM users WHERE id = $1';
const result = await pool.query(query, [userId]);
return result.rows[0];
}
// Using an ORM (Prisma example)
const user = await prisma.user.findUnique({
where: { id: userId }
});
3. NoSQL Injection Prevention
// MongoDB with Mongoose - built-in protection
const User = require('./models/User');
// Safe query
async function findUser(email) {
// Mongoose automatically sanitizes
return await User.findOne({ email });
}
// Additional validation for raw MongoDB
function sanitizeMongoQuery(query) {
if (typeof query !== 'object' || query === null) {
return {};
}
const sanitized = {};
for (const [key, value] of Object.entries(query)) {
if (typeof value === 'string') {
sanitized[key] = value;
}
}
return sanitized;
}
Cross-Site Scripting (XSS) Prevention
1. Output Encoding
// HTML encoding function
function htmlEncode(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// Using a template engine with auto-escaping (Handlebars)
app.engine('handlebars', exphbs({
defaultLayout: 'main',
helpers: {
json: function(context) {
return JSON.stringify(context);
}
}
}));
2. Client-Side XSS Prevention
// Safe DOM manipulation
function safeSetTextContent(element, text) {
element.textContent = text; // Safe - won't execute scripts
}
// Avoid innerHTML with user data
function unsafeSetHTML(element, html) {
element.innerHTML = html; // Dangerous!
}
// Use DOMPurify for HTML content
function safeSetHTML(element, html) {
element.innerHTML = DOMPurify.sanitize(html);
}
Cross-Site Request Forgery (CSRF) Protection
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();
});
// In your form template
// <input type="hidden" name="_csrf" value="<%= csrfToken %>">
Session Security
const session = require('express-session');
const MongoStore = require('connect-mongo');
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI
}),
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only
httpOnly: true, // Prevent XSS
maxAge: 1000 * 60 * 60 * 24, // 24 hours
sameSite: 'strict' // CSRF protection
}
}));
Rate Limiting and DDoS Protection
const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');
// Basic 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'
});
// Slow down repeated requests
const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000,
delayAfter: 50,
delayMs: 500
});
// Apply to all routes
app.use(limiter);
app.use(speedLimiter);
// Stricter limits for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true
});
app.use('/auth/login', authLimiter);
Security Headers
const helmet = require('helmet');
// Use Helmet for security headers
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "https:"],
scriptSrc: ["'self'"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
// Additional security headers
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
next();
});
File Upload Security
const multer = require('multer');
const path = require('path');
// Secure file upload configuration
const upload = multer({
dest: 'uploads/',
limits: {
fileSize: 5 * 1024 * 1024, // 5MB limit
files: 1
},
fileFilter: (req, file, cb) => {
// Allow only specific file types
const allowedTypes = /jpeg|jpg|png|gif|pdf/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (mimetype && extname) {
return cb(null, true);
} else {
cb(new Error('Invalid file type'));
}
}
});
// Scan uploaded files for malware (using ClamAV)
const NodeClam = require('clamscan');
async function scanFile(filePath) {
const clamscan = await new NodeClam().init();
const scanResult = await clamscan.scanFile(filePath);
return scanResult.isInfected === false;
}
API Security
// API key authentication
function validateApiKey(req, res, next) {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ error: 'API key required' });
}
// Validate API key (check against database)
if (!isValidApiKey(apiKey)) {
return res.status(401).json({ error: 'Invalid API key' });
}
next();
}
// Request signing for sensitive operations
const crypto = require('crypto');
function verifySignature(req, res, next) {
const signature = req.headers['x-signature'];
const payload = JSON.stringify(req.body);
const expectedSignature = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(payload)
.digest('hex');
if (signature !== `sha256=${expectedSignature}`) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
}
Logging and Monitoring
const winston = require('winston');
// Security-focused logging
const securityLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'security.log' }),
new winston.transports.Console()
]
});
// Log security events
function logSecurityEvent(event, details) {
securityLogger.warn('Security Event', {
event,
details,
timestamp: new Date().toISOString(),
ip: details.ip,
userAgent: details.userAgent
});
}
// Monitor failed login attempts
const failedAttempts = new Map();
function trackFailedLogin(ip) {
const attempts = failedAttempts.get(ip) || 0;
failedAttempts.set(ip, attempts + 1);
if (attempts >= 5) {
logSecurityEvent('BRUTE_FORCE_ATTEMPT', { ip, attempts });
// Implement IP blocking logic
}
}
Security Testing
1. Automated Security Testing
// Security testing with Jest
describe('Security Tests', () => {
test('should reject SQL injection attempts', async () => {
const maliciousInput = "'; DROP TABLE users; --";
const response = await request(app)
.post('/api/users')
.send({ name: maliciousInput });
expect(response.status).toBe(400);
});
test('should sanitize XSS attempts', () => {
const maliciousScript = '<script>alert("xss")</script>';
const sanitized = sanitizeHTML(maliciousScript);
expect(sanitized).not.toContain('<script>');
});
});
2. Security Audit Tools
# NPM audit for vulnerable dependencies
npm audit
# Security linting with ESLint security plugin
npm install eslint-plugin-security --save-dev
# OWASP ZAP for penetration testing
docker run -t owasp/zap2docker-stable zap-baseline.py -t http://localhost:3000
Conclusion
Web security is an ongoing process, not a one-time implementation. Regularly update dependencies, conduct security audits, and stay informed about new vulnerabilities and attack vectors.
Remember the principle of defense in depth—implement multiple layers of security rather than relying on a single protection mechanism. Security should be considered at every stage of development, from design to deployment and maintenance.
Resources
Security is everyone's responsibility. By implementing these practices, you're not just protecting your application—you're protecting your users' data and privacy. Stay vigilant, stay updated, and always assume that attackers are trying to find ways into your system.
About Tridip Dutta
Creative Developer passionate about creating innovative digital experiences and exploring AI. I love sharing knowledge to help developers build better apps.
