Back to the blog

Web Application Security Checklist

Complete security checklist: from HTTPS to XSS and CSRF protection.

January 5, 2026
12 min read
497 views
MOLOTILO

MOLOTILO DIGITAL

Web Application Security Checklist

Introduction

Web application security is not an option but a necessity. Let’s go through a complete security checklist for every developer.

1. HTTPS everywhere

server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    add_header Strict-Transport-Security "max-age=63072000" always;
}

2. Protection against XSS

// ❌ Dangerous
element.innerHTML = userInput;

// ✅ Safe
function escapeHtml(text: string): string {
  const map: Record<string, string> = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;'
  };
  return text.replace(/[&<>"']/g, char => map[char]);
}

element.textContent = userInput;

// React escapes automatically
function Comment({ text }: { text: string }) {
  return <p>{text}</p>;
}

// With HTML sanitization
import DOMPurify from 'dompurify';
const sanitizedHtml = DOMPurify.sanitize(html);

3. Content Security Policy

// next.config.js
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: [
      "default-src 'self'",
      "script-src 'self' 'unsafe-inline'",
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
      "connect-src 'self' https://api.example.com",
    ].join('; ')
  }
];

4. Protection against CSRF

import { randomBytes } from 'crypto';

function generateCsrfToken(): string {
  return randomBytes(32).toString('hex');
}

// Verification middleware
function csrfProtection(req, res, next) {
  if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
    const token = req.headers['x-csrf-token'];
    if (token !== req.session.csrfToken) {
      return res.status(403).json({ error: 'Invalid CSRF token' });
    }
  }
  next();
}

5. SQL Injection

// ❌ Dangerous
const query = `SELECT * FROM users WHERE id = ${userId}`;

// ✅ Parameterized queries
const user = await prisma.user.findUnique({
  where: { id: userId }
});

// ✅ With raw SQL
const users = await prisma.$queryRaw`
  SELECT * FROM users WHERE id = ${userId}
`;

6. Password security

import bcrypt from 'bcrypt';

const SALT_ROUNDS = 12;

async function hashPassword(password: string): Promise<string> {
  return bcrypt.hash(password, SALT_ROUNDS);
}

async function verifyPassword(password: string, hash: string): Promise<boolean> {
  return bcrypt.compare(password, hash);
}

Security checklist

  1. ✅ HTTPS on all pages
  2. ✅ CSP headers configured
  3. ✅ CSRF tokens for forms
  4. ✅ Parameterized SQL queries
  5. ✅ Passwords are hashed (bcrypt)
  6. ✅ Rate limiting on the API
  7. ✅ Input validation
  8. ✅ Secure HTTP headers

Security is a process, not a state. Regularly audit and update your dependencies.

7. Rate Limiting

import rateLimit from 'express-rate-limit';

// Basic rate limiter
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // maximum 100 requests
  message: 'Too many requests, please try again later.',
  standardHeaders: true,
  legacyHeaders: false,
});

// Strict limit for authentication
const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 5, // 5 attempts
  message: 'Too many login attempts',
});

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

8. Input validation

import { z } from 'zod';

// Validation schema
const UserSchema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Must contain uppercase')
    .regex(/[0-9]/, 'Must contain number'),
  age: z.number().min(18).max(120).optional(),
});

// Usage
async function createUser(req: Request, res: Response) {
  const result = UserSchema.safeParse(req.body);

  if (!result.success) {
    return res.status(400).json({
      errors: result.error.flatten().fieldErrors
    });
  }

  const validatedData = result.data;
  // Safe to use the data
}

9. Secure HTTP headers

import helmet from 'helmet';

app.use(helmet());

// Or configure manually
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true,
  },
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));

10. Security logging

import winston from 'winston';

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

// Logging suspicious activity
function logSecurityEvent(event: string, details: object) {
  securityLogger.warn({
    timestamp: new Date().toISOString(),
    event,
    ...details,
  });
}

// Usage
logSecurityEvent('FAILED_LOGIN', {
  ip: req.ip,
  email: req.body.email,
  userAgent: req.headers['user-agent'],
});

Authentication and authorization

Authentication verifies a user’s identity, authorization verifies their access rights. Use JWT tokens or session cookies to store state.

Password hashing

Never store passwords in plain text. Hashing with bcrypt or Argon2 protects the data even if the database leaks. Encrypting data in transit (HTTPS) and at rest is mandatory.

Protection against vulnerabilities

Main types of attacks and defenses:

  • XSS — sanitizing user input
  • CSRF — tokens and Origin checks
  • SQL Injection — parameterized queries

Configure CORS correctly — allow only trusted domains. Cookies must have the HttpOnly, Secure and SameSite flags.

Enjoyed the article?

Subscribe to our blog so you don’t miss new posts