Web Security Checklist for Developers (2026)

A practical, copy-paste-ready security checklist covering everything from HTTP headers to authentication. Stop your app from becoming the next breach headline.

Why Web Security Matters (The Numbers)

In 2025, the average cost of a data breach reached $4.88 million globally, according to IBM's Cost of a Data Breach Report. For small businesses, a single breach can be existential. And yet, the majority of breaches exploit known, preventable vulnerabilities — the kind this checklist covers.

Security is not just about protecting data. It is about trust. A security incident erodes user confidence, triggers regulatory fines (GDPR penalties can reach 4% of global revenue), and creates engineering work that could have been avoided with proper practices from the start.

This guide is written for developers who build web applications. It is practical, opinionated, and organized as a checklist you can work through item by item. Every recommendation includes code examples or configuration snippets you can apply immediately.

Reality Check

Over 60% of breaches in 2025 involved a web application vulnerability. The top causes: broken access control, injection flaws, and security misconfiguration. All preventable. All covered in this guide.

The OWASP Top 10 (2026 Edition)

The OWASP Top 10 is the definitive list of the most critical web application security risks. Here is the current ranking with brief descriptions and your immediate action items:

# Vulnerability What It Means
1 Broken Access Control Users can act outside their permissions. Accessing other users' data, admin functions without admin role.
2 Cryptographic Failures Sensitive data exposed due to weak encryption, plaintext storage, or missing encryption in transit.
3 Injection Untrusted data sent to an interpreter (SQL, NoSQL, OS command, LDAP). Includes XSS.
4 Insecure Design Missing security controls in the architecture. No threat modeling, no defense-in-depth.
5 Security Misconfiguration Default credentials, unnecessary features, verbose error messages, missing security headers.
6 Vulnerable Components Using libraries, frameworks, or dependencies with known vulnerabilities.
7 Authentication Failures Broken login, session management, or identity verification allowing account takeover.
8 Data Integrity Failures Unverified software updates, insecure CI/CD pipelines, unsigned data.
9 Logging & Monitoring Failures Insufficient logging makes breaches undetectable. No alerting on suspicious activity.
10 SSRF Server-Side Request Forgery. Server fetches a URL supplied by the attacker, accessing internal systems.

The rest of this guide walks through the practical defenses for each category.

HTTPS and TLS Configuration

HTTPS is the baseline. Every web application in 2026 must serve all traffic over HTTPS. Here is the checklist:

nginx — TLS Configuration
server {
    listen 443 ssl http2;
    server_name example.com;

    # TLS 1.2 and 1.3 only
    ssl_protocols TLSv1.2 TLSv1.3;

    # Strong cipher suites
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
    ssl_prefer_server_ciphers on;

    # HSTS (1 year, include subdomains)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # Certificate (Let's Encrypt)
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

Need to generate server configuration files quickly? NexTool's .htaccess Generator creates secure Apache configurations with a few clicks.

Essential Security Headers

Security headers are your first line of defense. They instruct browsers how to behave when loading your site. A missing header is an open door.

nginx — Security Headers
# Prevent clickjacking
add_header X-Frame-Options "SAMEORIGIN" always;

# Prevent MIME type sniffing
add_header X-Content-Type-Options "nosniff" always;

# XSS protection (legacy browsers)
add_header X-XSS-Protection "1; mode=block" always;

# Control referrer information
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Restrict browser features
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;

# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

# Content Security Policy (see next section)
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; frame-ancestors 'none';" always;
express.js — Security Headers with Helmet
import helmet from 'helmet';
import express from 'express';

const app = express();

// Helmet sets most security headers automatically
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      fontSrc: ["'self'", "https://fonts.gstatic.com"],
      connectSrc: ["'self'"],
      frameAncestors: ["'none'"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true,
  },
  referrerPolicy: {
    policy: 'strict-origin-when-cross-origin',
  },
}));

Use NexTool's Meta Tag Generator to create properly formatted security-related meta tags for your HTML documents.

Content Security Policy (CSP) Deep Dive

Content Security Policy is the single most effective defense against XSS attacks. It tells the browser exactly which sources of content are allowed, blocking everything else.

Building a CSP from Scratch

Start strict and loosen only as needed. Here is a step-by-step approach:

http header — Step 1: Start with Report-Only
Content-Security-Policy-Report-Only: default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'; report-uri /csp-report

The Report-Only mode logs violations without blocking anything. Deploy this first and monitor the reports. You will discover which third-party resources your site loads.

http header — Step 2: Add Necessary Sources
Content-Security-Policy: default-src 'none';
  script-src 'self' https://cdn.example.com;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  img-src 'self' data: https:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.example.com;
  frame-src 'none';
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  upgrade-insecure-requests;
  report-uri /csp-report

CSP with Nonces (Best Practice)

Instead of allowing 'unsafe-inline' for scripts (which defeats much of CSP's purpose), use nonces:

javascript — Express Middleware for CSP Nonces
import crypto from 'crypto';

app.use((req, res, next) => {
  // Generate a unique nonce for each request
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.nonce = nonce;

  res.setHeader('Content-Security-Policy',
    `default-src 'self'; ` +
    `script-src 'self' 'nonce-${nonce}'; ` +
    `style-src 'self' 'nonce-${nonce}'; ` +
    `img-src 'self' data: https:; ` +
    `font-src 'self' https://fonts.gstatic.com; ` +
    `connect-src 'self'; ` +
    `frame-ancestors 'none';`
  );

  next();
});
html — Using the Nonce in Templates
<!-- Only scripts with the matching nonce will execute -->
<script nonce="<%= nonce %>">
  console.log('This script is allowed by CSP');
</script>

<!-- This inline script will be BLOCKED -->
<script>
  console.log('This will be blocked by CSP');
</script>
Best Practice

Always start with default-src 'none' and explicitly allow only what you need. This "deny by default" approach means new attack vectors are blocked automatically.

XSS Prevention

Cross-Site Scripting (XSS) remains one of the most common web vulnerabilities. An attacker injects malicious scripts that execute in other users' browsers, stealing sessions, credentials, or performing actions on their behalf.

Types of XSS

Prevention Checklist

javascript — Proper Output Encoding
// DANGEROUS: Direct HTML insertion
element.innerHTML = userInput;  // XSS vulnerability!

// SAFE: Text content (auto-escapes HTML entities)
element.textContent = userInput;

// SAFE: Use DOMPurify when HTML is needed
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);

// SAFE: React JSX auto-escapes by default
function Comment({ text }) {
  return <p>{text}</p>;  // Automatically escaped
}

// DANGEROUS: React dangerouslySetInnerHTML
function Comment({ html }) {
  // Only use with DOMPurify
  return <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />;
}

SQL Injection Prevention

SQL injection occurs when user input is concatenated into SQL queries without proper parameterization. It can lead to full database compromise, data theft, and even remote code execution.

Never Do This

String concatenation in SQL queries is the number one cause of SQL injection. It does not matter how much you "validate" the input — parameterized queries are the only reliable defense.

javascript — Node.js SQL Injection Prevention
// VULNERABLE: String concatenation
const query = `SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`;
// Attacker input: email = "' OR '1'='1" → Bypasses authentication

// SAFE: Parameterized query (pg library)
const result = await pool.query(
  'SELECT * FROM users WHERE email = $1 AND password_hash = $2',
  [email, passwordHash]
);

// SAFE: Parameterized query (mysql2 library)
const [rows] = await connection.execute(
  'SELECT * FROM users WHERE email = ? AND password_hash = ?',
  [email, passwordHash]
);

// SAFE: Using an ORM (Prisma)
const user = await prisma.user.findUnique({
  where: { email: email }
});

// SAFE: Using an ORM (Drizzle)
const user = await db.select()
  .from(users)
  .where(eq(users.email, email));
python — SQL Injection Prevention
# VULNERABLE: String formatting
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")

# SAFE: Parameterized query
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))

# SAFE: SQLAlchemy ORM
user = session.query(User).filter(User.email == email).first()

# SAFE: SQLAlchemy Core with parameters
stmt = text("SELECT * FROM users WHERE email = :email")
result = connection.execute(stmt, {"email": email})

CORS Configuration

Cross-Origin Resource Sharing (CORS) controls which domains can make requests to your API. Misconfigured CORS is a common vulnerability that allows malicious websites to make authenticated requests on behalf of your users.

javascript — Secure CORS Configuration (Express)
import cors from 'cors';

// DANGEROUS: Allow all origins
app.use(cors());  // Never do this in production!

// SAFE: Whitelist specific origins
const allowedOrigins = [
  'https://yourdomain.com',
  'https://app.yourdomain.com',
];

app.use(cors({
  origin: (origin, callback) => {
    // Allow requests with no origin (mobile apps, curl, etc.)
    if (!origin) return callback(null, true);

    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,          // Allow cookies
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  maxAge: 86400,              // Cache preflight for 24 hours
}));

Authentication Best Practices

Authentication is the gate to your application. A weakness here compromises everything behind it.

javascript — Rate Limiting Login Endpoint
import rateLimit from 'express-rate-limit';

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 5,                     // 5 attempts per window
  message: {
    error: 'Too many login attempts. Please try again in 15 minutes.'
  },
  standardHeaders: true,       // Return rate limit info in headers
  legacyHeaders: false,
  keyGenerator: (req) => {
    // Rate limit by IP + email combination
    return `${req.ip}-${req.body.email}`;
  },
});

app.post('/api/auth/login', loginLimiter, async (req, res) => {
  // ... authentication logic
});

Password Storage and Policy

If your application stores passwords (as opposed to using OAuth/SSO exclusively), getting this right is non-negotiable.

javascript — Password Hashing with bcrypt
import bcrypt from 'bcrypt';

const SALT_ROUNDS = 12;  // Cost factor: 12 is good for 2026 hardware

// Hash a password before storing
async function hashPassword(plainPassword) {
  return await bcrypt.hash(plainPassword, SALT_ROUNDS);
}

// Verify a password during login
async function verifyPassword(plainPassword, storedHash) {
  return await bcrypt.compare(plainPassword, storedHash);
}

// Usage
const hash = await hashPassword('user_password_here');
// Store hash in database: $2b$12$LJ3m4ys3Lg...

const isValid = await verifyPassword('user_password_here', hash);
// Returns true or false
python — Password Hashing with Argon2
from argon2 import PasswordHasher

ph = PasswordHasher(
    time_cost=3,        # Number of iterations
    memory_cost=65536,  # 64 MB memory usage
    parallelism=4,      # Number of parallel threads
)

# Hash a password
hash = ph.hash("user_password_here")
# $argon2id$v=19$m=65536,t=3,p=4$...

# Verify a password
try:
    ph.verify(hash, "user_password_here")
    print("Password is correct")
except Exception:
    print("Password is incorrect")

# Check if rehashing is needed (after upgrading parameters)
if ph.check_needs_rehash(hash):
    new_hash = ph.hash("user_password_here")
    # Update hash in database

Need to generate strong passwords for testing or personal use? Try NexTool's Password Generator. For verifying hash outputs, use the Hash Generator.

Session Management

Sessions are the bridge between authentication and authorization. Mismanaging sessions can undo all your authentication security.

javascript — Secure Session Configuration (Express)
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';

const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  name: '__session',              // Custom cookie name (not 'connect.sid')
  secret: process.env.SESSION_SECRET,  // Strong, random secret
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,                 // HTTPS only
    httpOnly: true,               // No JavaScript access
    sameSite: 'lax',              // CSRF protection
    maxAge: 24 * 60 * 60 * 1000,  // 24 hours
    domain: '.yourdomain.com',    // Scope to your domain
    path: '/',
  },
}));

// Regenerate session ID after login (prevent session fixation)
app.post('/api/auth/login', async (req, res) => {
  const user = await authenticate(req.body.email, req.body.password);

  if (user) {
    req.session.regenerate((err) => {
      if (err) return res.status(500).json({ error: 'Session error' });

      req.session.userId = user.id;
      req.session.save((err) => {
        if (err) return res.status(500).json({ error: 'Session error' });
        res.json({ success: true });
      });
    });
  }
});

Dependency Security

Your application is only as secure as its weakest dependency. Supply chain attacks — where attackers compromise popular packages — increased 742% between 2019 and 2025.

bash — Auditing Dependencies
# Node.js / npm
npm audit
npm audit fix

# Python / pip
pip-audit
safety check

# Go
govulncheck ./...

# Rust
cargo audit

# Generic (works with many ecosystems)
snyk test
trivy fs .
yaml — GitHub Actions Dependency Audit
name: Security Audit

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 8 * * 1'  # Every Monday at 8 AM

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Install dependencies
        run: npm ci

      - name: Run security audit
        run: npm audit --audit-level=high

      - name: Check for known vulnerabilities
        run: npx better-npm-audit audit --level high

Deployment Security Checklist

Security does not end with code. Your deployment environment needs hardening too. Here is the deployment checklist:

nginx — Hardened Production Config
# Hide server version
server_tokens off;

# Disable unnecessary methods
if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE|PATCH)$) {
    return 405;
}

# Block access to hidden files (.env, .git, etc.)
location ~ /\. {
    deny all;
    access_log off;
    log_not_found off;
}

# Block access to backup files
location ~* \.(bak|config|sql|fla|psd|ini|log|sh|inc|swp|dist)$ {
    deny all;
}

# Limit request body size (prevent large upload attacks)
client_max_body_size 10m;

# Timeout settings (prevent slow loris attacks)
client_body_timeout 10;
client_header_timeout 10;
send_timeout 10;

Generate Secure Configurations Instantly

Use NexTool's free tools to generate security-focused server configs, meta tags, and robots.txt files.

.htaccess Generator All Free Tools

Security Tools

These free NexTool tools help you implement the security practices covered in this guide:


Frequently Asked Questions

The most critical web security vulnerabilities in 2026 according to the OWASP Top 10 are: Broken Access Control (authorization flaws allowing users to act beyond their permissions), Cryptographic Failures (weak encryption or exposed sensitive data), Injection (SQL injection, XSS, command injection), Insecure Design (missing security architecture and threat modeling), and Security Misconfiguration (default credentials, verbose errors, missing headers). Server-Side Request Forgery (SSRF) and supply chain attacks through compromised dependencies have also become increasingly prevalent.

Every website should include these security headers: Content-Security-Policy (CSP) to prevent XSS and data injection, Strict-Transport-Security (HSTS) to enforce HTTPS, X-Content-Type-Options: nosniff to prevent MIME type sniffing, X-Frame-Options: DENY or SAMEORIGIN to prevent clickjacking, Referrer-Policy to control referrer information leakage, and Permissions-Policy to restrict browser features. These can be configured in your web server (Nginx, Apache), CDN (Cloudflare), or application framework middleware like Helmet.js.

Prevent XSS by following these practices: Always encode user input before rendering in HTML (use framework-provided escaping like React's JSX or template engine auto-escaping), implement a strict Content Security Policy (CSP) that disallows inline scripts using nonces, use HttpOnly and Secure flags on cookies to prevent session theft, validate and sanitize input on the server side, use the DOMPurify library when you need to render user-provided HTML, and avoid using innerHTML or dangerouslySetInnerHTML with unsanitized data.

Never store passwords in plain text. Use a strong, slow hashing algorithm designed specifically for passwords: bcrypt (with cost factor 12+), scrypt, or Argon2id (recommended as the most modern option). Each of these algorithms automatically handles salting. Never use fast hashing algorithms like MD5, SHA-1, or SHA-256 for passwords — they can be brute-forced at billions of attempts per second on modern GPUs. Additionally, implement rate limiting on login endpoints, enforce a minimum password length of 12 characters, and consider using Have I Been Pwned's API to reject previously breached passwords.

NT

NexTool Team

We build developer tools and write security guides to help you ship safer software. Our free tools are used by thousands of developers worldwide.

Need a Security Audit?

NexTool offers custom security reviews and automation. We identify vulnerabilities and help you fix them before attackers find them.

Get a Free Quote