← Back to Blog

Security Best Practices for Web Applications

Web application security is not optional—it's essential. A single vulnerability can lead to data breaches, financial losses, and irreparable damage to your reputation. This guide covers the most critical security practices every web developer must implement to protect their applications and users.

The OWASP Top 10

The Open Web Application Security Project (OWASP) maintains a list of the most critical web application security risks. Understanding and mitigating these is your first line of defense.

  1. Broken Access Control
  2. Cryptographic Failures
  3. Injection
  4. Insecure Design
  5. Security Misconfiguration
  6. Vulnerable and Outdated Components
  7. Identification and Authentication Failures
  8. Software and Data Integrity Failures
  9. Security Logging and Monitoring Failures
  10. Server-Side Request Forgery (SSRF)

Authentication and Authorization

Secure Password Handling

Never store passwords in plain text. Always use strong hashing algorithms like bcrypt, Argon2, or scrypt.

// Using bcrypt in Node.js
import bcrypt from 'bcrypt';

// Hash password on signup
async function hashPassword(password) {
  const saltRounds = 12;
  const hashedPassword = await bcrypt.hash(password, saltRounds);
  return hashedPassword;
}

// Verify password on login
async function verifyPassword(password, hashedPassword) {
  const isMatch = await bcrypt.compare(password, hashedPassword);
  return isMatch;
}

// DON'T DO THIS
const badHash = crypto.createHash('md5').update(password).digest('hex'); // INSECURE!

Password Requirements

Multi-Factor Authentication (MFA)

Implement MFA for sensitive operations. Use authenticator apps (TOTP) rather than SMS when possible.

// Implementing TOTP with speakeasy
import speakeasy from 'speakeasy';
import QRCode from 'qrcode';

// Generate secret
const secret = speakeasy.generateSecret({
  name: 'YourApp (user@example.com)',
  length: 32,
});

// Generate QR code for user
const qrCode = await QRCode.toDataURL(secret.otpauth_url);

// Verify token
const verified = speakeasy.totp.verify({
  secret: secret.base32,
  encoding: 'base32',
  token: userEnteredToken,
  window: 2, // Allow 2 time steps before/after
});

JSON Web Tokens (JWT) Best Practices

import jwt from 'jsonwebtoken';

// Generate token
function generateToken(userId) {
  return jwt.sign(
    { userId },
    process.env.JWT_SECRET,
    {
      expiresIn: '15m', // Short-lived access token
      issuer: 'your-app',
      audience: 'your-app-users',
    }
  );
}

// Verify token
function verifyToken(token) {
  try {
    return jwt.verify(token, process.env.JWT_SECRET, {
      issuer: 'your-app',
      audience: 'your-app-users',
    });
  } catch (error) {
    throw new Error('Invalid token');
  }
}

// Use refresh tokens for long-term sessions
function generateRefreshToken(userId) {
  return jwt.sign(
    { userId },
    process.env.REFRESH_TOKEN_SECRET,
    { expiresIn: '7d' }
  );
}

JWT Security Tips

Cross-Site Scripting (XSS) Prevention

XSS attacks inject malicious scripts into your web pages. Prevent them by properly escaping user input and implementing Content Security Policy (CSP).

Input Sanitization

// Using DOMPurify in the browser
import DOMPurify from 'dompurify';

// Sanitize HTML input
const clean = DOMPurify.sanitize(userInput);
document.getElementById('content').innerHTML = clean;

// React automatically escapes text content
function UserProfile({ username }) {
  // This is safe - React escapes automatically
  return 
{username}
; // This is DANGEROUS - bypasses React's protection return
; // DON'T DO THIS }

Content Security Policy (CSP)

// Next.js - next.config.js
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: `
      default-src 'self';
      script-src 'self' 'unsafe-eval' 'unsafe-inline' https://trusted-cdn.com;
      style-src 'self' 'unsafe-inline';
      img-src 'self' data: https:;
      font-src 'self' data:;
      connect-src 'self' https://api.yourapp.com;
      frame-ancestors 'none';
    `.replace(/\s{2,}/g, ' ').trim()
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff'
  },
  {
    key: 'X-Frame-Options',
    value: 'DENY'
  },
  {
    key: 'X-XSS-Protection',
    value: '1; mode=block'
  },
  {
    key: 'Referrer-Policy',
    value: 'strict-origin-when-cross-origin'
  }
];

module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: securityHeaders,
      },
    ];
  },
};

Cross-Site Request Forgery (CSRF) Protection

CSRF attacks trick users into performing unwanted actions. Protect against them using CSRF tokens.

CSRF Token Implementation

// Express with csurf middleware
import csrf from 'csurf';
import cookieParser from 'cookie-parser';

const csrfProtection = csrf({ cookie: true });

app.use(cookieParser());

// Send CSRF token to client
app.get('/api/csrf-token', csrfProtection, (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

// Protect state-changing endpoints
app.post('/api/transfer-money', csrfProtection, (req, res) => {
  // This will fail without valid CSRF token
  // Process transfer...
});
// Frontend - Include CSRF token in requests
async function makeSecureRequest(url, data) {
  // Get CSRF token
  const response = await fetch('/api/csrf-token');
  const { csrfToken } = await response.json();

  // Include in request
  return fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'CSRF-Token': csrfToken,
    },
    body: JSON.stringify(data),
  });
}

SameSite Cookies

// Set secure cookie options
res.cookie('sessionId', sessionId, {
  httpOnly: true,      // Prevent JavaScript access
  secure: true,        // HTTPS only
  sameSite: 'strict',  // Prevent CSRF
  maxAge: 3600000,     // 1 hour
  path: '/',
  domain: 'yourapp.com',
});

SQL Injection Prevention

Never concatenate user input into SQL queries. Always use parameterized queries or ORMs.

// VULNERABLE - Don't do this
const userId = req.params.id;
const query = `SELECT * FROM users WHERE id = ${userId}`; // SQL INJECTION!

// SAFE - Use parameterized queries
const query = 'SELECT * FROM users WHERE id = ?';
const result = await db.query(query, [userId]);

// SAFE - Using Prisma ORM
const user = await prisma.user.findUnique({
  where: { id: userId }
});

// SAFE - Using PostgreSQL node-postgres
const result = await client.query(
  'SELECT * FROM users WHERE id = $1',
  [userId]
);

Secure Session Management

Session Configuration

import session from 'express-session';
import RedisStore from 'connect-redis';
import Redis from 'ioredis';

const redisClient = new Redis({
  host: process.env.REDIS_HOST,
  port: process.env.REDIS_PORT,
});

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,           // HTTPS only
    httpOnly: true,         // No JavaScript access
    maxAge: 3600000,        // 1 hour
    sameSite: 'strict',     // CSRF protection
  },
  name: 'sessionId',        // Don't use default 'connect.sid'
}));

Session Best Practices

// Regenerate session on login
app.post('/login', async (req, res) => {
  const user = await authenticateUser(req.body);

  if (user) {
    // Regenerate session to prevent fixation
    req.session.regenerate((err) => {
      if (err) return res.status(500).json({ error: 'Login failed' });

      req.session.userId = user.id;
      req.session.save(() => {
        res.json({ success: true });
      });
    });
  }
});

// Destroy session on logout
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) return res.status(500).json({ error: 'Logout failed' });
    res.clearCookie('sessionId');
    res.json({ success: true });
  });
});

Rate Limiting and DDoS Protection

import rateLimit from 'express-rate-limit';

// General API rate limit
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 requests per window
  message: 'Too many requests, please try again later',
  standardHeaders: true,
  legacyHeaders: false,
});

// Stricter limit for authentication endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5, // Only 5 login attempts per 15 minutes
  skipSuccessfulRequests: true, // Don't count successful logins
});

app.use('/api/', apiLimiter);
app.use('/api/auth/', authLimiter);

HTTPS and TLS Configuration

Always Use HTTPS

Never transmit sensitive data over HTTP. Use HTTPS everywhere, enforce it with HSTS, and get free certificates from Let's Encrypt.

// Enforce HTTPS redirection
app.use((req, res, next) => {
  if (req.header('x-forwarded-proto') !== 'https' && process.env.NODE_ENV === 'production') {
    res.redirect(`https://${req.header('host')}${req.url}`);
  } else {
    next();
  }
});

// Set HSTS header
app.use((req, res, next) => {
  res.setHeader(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains; preload'
  );
  next();
});

Input Validation

Never trust user input. Validate everything on both client and server side.

// Using Zod for validation
import { z } from 'zod';

const userSchema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(12, 'Password must be at least 12 characters'),
  age: z.number().min(18).max(120),
  username: z.string().regex(/^[a-zA-Z0-9_]+$/, 'Invalid username'),
});

app.post('/api/register', async (req, res) => {
  try {
    // Validate input
    const validatedData = userSchema.parse(req.body);

    // Process registration...
    res.json({ success: true });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return res.status(400).json({ errors: error.errors });
    }
    res.status(500).json({ error: 'Server error' });
  }
});

Security Logging and Monitoring

// Log security events
import winston from 'winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'security.log' }),
  ],
});

// Log authentication attempts
function logAuthAttempt(userId, success, ip) {
  logger.info('Authentication attempt', {
    userId,
    success,
    ip,
    timestamp: new Date().toISOString(),
  });
}

// Log suspicious activity
function logSuspiciousActivity(type, details, ip) {
  logger.warn('Suspicious activity detected', {
    type,
    details,
    ip,
    timestamp: new Date().toISOString(),
  });
}

Dependency Security

# Regularly audit dependencies
npm audit

# Fix vulnerabilities automatically
npm audit fix

# Use tools like Snyk or GitHub Dependabot
# to monitor dependencies continuously

Security Checklist

Environment Variables and Secrets

// Never commit secrets to version control
// Use .env files and .gitignore

// .env (NOT committed)
DATABASE_URL=postgresql://user:password@localhost:5432/db
JWT_SECRET=your-super-secret-key-min-256-bits
STRIPE_SECRET_KEY=sk_test_...

// .gitignore
.env
.env.local
.env.production

// Access in code
import dotenv from 'dotenv';
dotenv.config();

const dbUrl = process.env.DATABASE_URL;

Conclusion

Web security is an ongoing process, not a one-time task. Regularly review your security practices, stay updated on new vulnerabilities, conduct security audits, and never assume your application is "secure enough."

Security is not a product, but a process. - Bruce Schneier

Implement these best practices, educate your team, and make security a core part of your development workflow. Your users trust you with their data—honor that trust by prioritizing security at every level of your application.

Need a Security Audit?

Yonda Solutions offers comprehensive security audits and implementation services. We can review your application, identify vulnerabilities, and help implement robust security measures. Contact us today to secure your web application.