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.

Web Security Best Practices 2025: Protecting Modern Applications - Daniel Gurczynski