Web Security Best Practices 2025: Protecting Modern Applications
Web security has become more critical than ever as applications handle sensitive data and face sophisticated threats. This comprehensive guide covers the essential security practices every developer should implement in 2025.
Authentication and Authorization
Modern Authentication Patterns
// JWT with refresh tokens
const jwt = require('jsonwebtoken')
const bcrypt = require('bcrypt')
class AuthService {
async login(email, password) {
const user = await User.findByEmail(email)
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
throw new Error('Invalid credentials')
}
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
)
const refreshToken = jwt.sign(
{ userId: user.id, tokenVersion: user.tokenVersion },
process.env.REFRESH_SECRET,
{ expiresIn: '7d' }
)
// Store refresh token securely
await this.storeRefreshToken(user.id, refreshToken)
return { accessToken, refreshToken }
}
async refreshAccessToken(refreshToken) {
try {
const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET)
const user = await User.findById(payload.userId)
if (!user || user.tokenVersion !== payload.tokenVersion) {
throw new Error('Invalid refresh token')
}
const newAccessToken = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
)
return { accessToken: newAccessToken }
} catch (error) {
throw new Error('Invalid refresh token')
}
}
}
OAuth 2.0 / OpenID Connect
// OAuth implementation with PKCE
const crypto = require('crypto')
class OAuthService {
generateAuthUrl(clientId, redirectUri, scopes) {
const state = crypto.randomBytes(32).toString('hex')
const codeVerifier = crypto.randomBytes(32).toString('base64url')
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url')
// Store state and code verifier securely
this.storeOAuthState(state, { codeVerifier, redirectUri })
const params = new URLSearchParams({
response_type: 'code',
client_id: clientId,
redirect_uri: redirectUri,
scope: scopes.join(' '),
state,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
})
return `https://oauth.provider.com/authorize?${params}`
}
async exchangeCodeForTokens(code, state) {
const storedState = await this.getOAuthState(state)
if (!storedState) {
throw new Error('Invalid state parameter')
}
const tokenResponse = await fetch('https://oauth.provider.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
client_id: process.env.OAUTH_CLIENT_ID,
code_verifier: storedState.codeVerifier,
redirect_uri: storedState.redirectUri
})
})
return tokenResponse.json()
}
}
Input Validation and Sanitization
Comprehensive Validation
const Joi = require('joi')
const DOMPurify = require('dompurify')
const { JSDOM } = require('jsdom')
const window = new JSDOM('').window
const purify = DOMPurify(window)
// Schema validation
const userSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/
).required(),
name: Joi.string().min(2).max(50).required(),
bio: Joi.string().max(500).optional()
})
const validateInput = (schema) => (req, res, next) => {
const { error, value } = schema.validate(req.body)
if (error) {
return res.status(400).json({
error: 'Validation failed',
details: error.details.map(d => d.message)
})
}
req.validatedData = value
next()
}
// HTML sanitization
const sanitizeHtml = (html) => {
return purify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
})
}
// SQL injection prevention
const db = require('pg')
const pool = new db.Pool()
const getUserById = async (userId) => {
// Use parameterized queries
const result = await pool.query(
'SELECT id, email, name FROM users WHERE id = $1',
[userId]
)
return result.rows[0]
}
Cross-Site Scripting (XSS) Prevention
Content Security Policy
// CSP middleware
const helmet = require('helmet')
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
"'unsafe-inline'", // Avoid this in production
"https://trusted-cdn.com"
],
styleSrc: [
"'self'",
"'unsafe-inline'",
"https://fonts.googleapis.com"
],
imgSrc: [
"'self'",
"data:",
"https:"
],
connectSrc: [
"'self'",
"https://api.example.com"
],
fontSrc: [
"'self'",
"https://fonts.gstatic.com"
],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"]
}
}))
Output Encoding
// Template engine with auto-escaping
const handlebars = require('handlebars')
// Register safe helper for trusted HTML
handlebars.registerHelper('safe', function(value) {
return new handlebars.SafeString(value)
})
// Template usage
const template = handlebars.compile(`
<div>
<h1>{{title}}</h1> <!-- Auto-escaped -->
<div>{{{safe content}}}</div> <!-- Trusted HTML -->
</div>
`)
// React JSX auto-escapes by default
const UserProfile = ({ user }) => (
<div>
<h1>{user.name}</h1> {/* Auto-escaped */}
<div dangerouslySetInnerHTML={{
__html: sanitizeHtml(user.bio) // Sanitized HTML
}} />
</div>
)
Cross-Site Request Forgery (CSRF) Protection
CSRF Token Implementation
const csrf = require('csurf')
const cookieParser = require('cookie-parser')
app.use(cookieParser())
// CSRF protection middleware
const csrfProtection = csrf({
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
}
})
app.use(csrfProtection)
// Provide token to client
app.get('/api/csrf-token', (req, res) => {
res.json({ csrfToken: req.csrfToken() })
})
// Validate token on state-changing requests
app.post('/api/users', csrfProtection, (req, res) => {
// CSRF token automatically validated
// Process request
})
SameSite Cookies
// Secure cookie configuration
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict', // Prevents CSRF attacks
maxAge: 24 * 60 * 60 * 1000 // 24 hours
},
resave: false,
saveUninitialized: false
}))
HTTPS and Transport Security
TLS Configuration
const https = require('https')
const fs = require('fs')
const options = {
key: fs.readFileSync('private-key.pem'),
cert: fs.readFileSync('certificate.pem'),
// Use strong cipher suites
ciphers: [
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES128-SHA256',
'ECDHE-RSA-AES256-SHA384'
].join(':'),
honorCipherOrder: true
}
https.createServer(options, app).listen(443)
// HTTP Strict Transport Security
app.use(helmet.hsts({
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true
}))
Certificate Pinning
// Public key pinning
app.use(helmet.hpkp({
maxAge: 7776000, // 90 days
sha256s: [
'base64-encoded-sha256-of-your-cert',
'base64-encoded-sha256-of-backup-cert'
],
includeSubDomains: true
}))
API Security
Rate Limiting
const rateLimit = require('express-rate-limit')
const RedisStore = require('rate-limit-redis')
const Redis = require('ioredis')
const redis = new Redis(process.env.REDIS_URL)
// General rate limiting
const generalLimiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redis.call(...args)
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
})
// Strict rate limiting for authentication
const authLimiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redis.call(...args)
}),
windowMs: 15 * 60 * 1000,
max: 5, // 5 attempts per 15 minutes
skipSuccessfulRequests: true,
message: 'Too many login attempts'
})
app.use('/api/', generalLimiter)
app.use('/api/auth/', authLimiter)
API Key Management
// API key authentication
const crypto = require('crypto')
class ApiKeyService {
generateApiKey() {
return crypto.randomBytes(32).toString('hex')
}
async createApiKey(userId, name, permissions) {
const key = this.generateApiKey()
const hashedKey = crypto
.createHash('sha256')
.update(key)
.digest('hex')
await ApiKey.create({
userId,
name,
hashedKey,
permissions,
createdAt: new Date()
})
return key // Return only once
}
async validateApiKey(key) {
const hashedKey = crypto
.createHash('sha256')
.update(key)
.digest('hex')
const apiKey = await ApiKey.findOne({
hashedKey,
isActive: true
})
if (!apiKey) {
throw new Error('Invalid API key')
}
// Update last used
await apiKey.update({ lastUsedAt: new Date() })
return apiKey
}
}
// API key middleware
const authenticateApiKey = async (req, res, next) => {
const apiKey = req.headers['x-api-key']
if (!apiKey) {
return res.status(401).json({ error: 'API key required' })
}
try {
const keyData = await apiKeyService.validateApiKey(apiKey)
req.apiKey = keyData
next()
} catch (error) {
res.status(401).json({ error: 'Invalid API key' })
}
}
Data Protection
Encryption at Rest
const crypto = require('crypto')
class EncryptionService {
constructor() {
this.algorithm = 'aes-256-gcm'
this.keyLength = 32
this.ivLength = 16
this.tagLength = 16
}
encrypt(text, key) {
const iv = crypto.randomBytes(this.ivLength)
const cipher = crypto.createCipher(this.algorithm, key, iv)
let encrypted = cipher.update(text, 'utf8', 'hex')
encrypted += cipher.final('hex')
const tag = cipher.getAuthTag()
return {
encrypted,
iv: iv.toString('hex'),
tag: tag.toString('hex')
}
}
decrypt(encryptedData, key) {
const decipher = crypto.createDecipher(
this.algorithm,
key,
Buffer.from(encryptedData.iv, 'hex')
)
decipher.setAuthTag(Buffer.from(encryptedData.tag, 'hex'))
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8')
decrypted += decipher.final('utf8')
return decrypted
}
}
// Database field encryption
class User extends Model {
static init(sequelize) {
super.init({
email: DataTypes.STRING,
encryptedSSN: DataTypes.TEXT,
ssnIv: DataTypes.STRING,
ssnTag: DataTypes.STRING
}, { sequelize })
}
setSSN(ssn) {
const encrypted = encryptionService.encrypt(ssn, process.env.ENCRYPTION_KEY)
this.encryptedSSN = encrypted.encrypted
this.ssnIv = encrypted.iv
this.ssnTag = encrypted.tag
}
getSSN() {
if (!this.encryptedSSN) return null
return encryptionService.decrypt({
encrypted: this.encryptedSSN,
iv: this.ssnIv,
tag: this.ssnTag
}, process.env.ENCRYPTION_KEY)
}
}
Personal Data Handling (GDPR/CCPA)
// Data anonymization
class DataAnonymizer {
anonymizeUser(user) {
return {
id: this.hashId(user.id),
age: this.ageRange(user.age),
location: this.generalizeLocation(user.location),
// Remove direct identifiers
email: null,
name: null,
phone: null
}
}
hashId(id) {
return crypto
.createHash('sha256')
.update(id + process.env.ANONYMIZATION_SALT)
.digest('hex')
.substring(0, 16)
}
ageRange(age) {
return `${Math.floor(age / 10) * 10}-${Math.floor(age / 10) * 10 + 9}`
}
generalizeLocation(location) {
// Return only country/region
return location.country
}
}
// Data retention policy
class DataRetentionService {
async enforceRetentionPolicy() {
const cutoffDate = new Date()
cutoffDate.setFullYear(cutoffDate.getFullYear() - 7) // 7 years
// Delete old user data
await User.destroy({
where: {
lastLoginAt: { [Op.lt]: cutoffDate },
deletedAt: null
}
})
// Anonymize old analytics data
const oldAnalytics = await Analytics.findAll({
where: { createdAt: { [Op.lt]: cutoffDate } }
})
for (const record of oldAnalytics) {
const anonymized = dataAnonymizer.anonymizeUser(record)
await record.update(anonymized)
}
}
}
Security Headers
Comprehensive Security Headers
const helmet = require('helmet')
app.use(helmet({
// Content Security Policy
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"]
}
},
// HTTP Strict Transport Security
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
// X-Frame-Options
frameguard: { action: 'deny' },
// X-Content-Type-Options
noSniff: true,
// Referrer Policy
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
// Permissions Policy
permissionsPolicy: {
camera: [],
microphone: [],
geolocation: ['self'],
notifications: ['self']
}
}))
// Custom security headers
app.use((req, res, next) => {
res.setHeader('X-XSS-Protection', '1; mode=block')
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none')
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
next()
})
Security Testing
Automated Security Testing
// Security test suite
const request = require('supertest')
const app = require('../app')
describe('Security Tests', () => {
test('should reject requests without CSRF token', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Test User' })
expect(response.status).toBe(403)
})
test('should sanitize HTML input', async () => {
const maliciousInput = '<script>alert("xss")</script>Hello'
const response = await request(app)
.post('/api/comments')
.send({ content: maliciousInput })
.set('X-CSRF-Token', csrfToken)
expect(response.body.content).not.toContain('<script>')
expect(response.body.content).toBe('Hello')
})
test('should enforce rate limiting', async () => {
const requests = Array(10).fill().map(() =>
request(app).post('/api/auth/login')
)
const responses = await Promise.all(requests)
const tooManyRequests = responses.filter(r => r.status === 429)
expect(tooManyRequests.length).toBeGreaterThan(0)
})
})
Incident Response
Security Monitoring
// Security event logging
class SecurityLogger {
logSecurityEvent(event, details) {
const logEntry = {
timestamp: new Date().toISOString(),
event,
details,
ip: details.ip,
userAgent: details.userAgent,
severity: this.getSeverity(event)
}
// Log to security system
console.log(JSON.stringify(logEntry))
// Alert on high severity events
if (logEntry.severity === 'HIGH') {
this.sendAlert(logEntry)
}
}
getSeverity(event) {
const highSeverityEvents = [
'MULTIPLE_FAILED_LOGINS',
'SQL_INJECTION_ATTEMPT',
'XSS_ATTEMPT',
'PRIVILEGE_ESCALATION'
]
return highSeverityEvents.includes(event) ? 'HIGH' : 'MEDIUM'
}
async sendAlert(logEntry) {
// Send to monitoring system
await fetch(process.env.ALERT_WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(logEntry)
})
}
}
Conclusion
Web security requires a multi-layered approach combining secure coding practices, proper configuration, and continuous monitoring. Key principles:
- Defense in depth: Multiple security layers
- Principle of least privilege: Minimal necessary access
- Fail securely: Secure defaults and error handling
- Keep it simple: Complexity increases attack surface
- Stay updated: Regular security patches and updates
Security is not a one-time implementation but an ongoing process. Regular security audits, penetration testing, and staying informed about emerging threats are essential for maintaining robust application security.
Remember: security is only as strong as its weakest link. Invest in security training for your team and make security a core part of your development process.
