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.
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:
-
✓
Enforce HTTPS everywhere. Redirect all HTTP traffic to HTTPS. No exceptions, not even for static assets or API endpoints. Critical
-
✓
Use TLS 1.3 (or TLS 1.2 minimum). Disable TLS 1.0 and 1.1. They have known vulnerabilities and are deprecated by all major browsers. Critical
-
✓
Enable HSTS (HTTP Strict Transport Security). Tells browsers to only connect via HTTPS, preventing SSL stripping attacks. Important
-
✓
Use strong cipher suites. Prefer AEAD ciphers (AES-GCM, ChaCha20-Poly1305). Disable CBC mode ciphers. Recommended
-
✓
Automate certificate renewal. Use Let's Encrypt with auto-renewal. Certificate expiry is a common cause of outages. Important
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.
# 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;
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:
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.
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:
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();
});
<!-- 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>
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
- Stored XSS — Malicious script is saved in the database (e.g., in a comment) and served to every user who views it. Most dangerous.
- Reflected XSS — Malicious script is reflected off a web server in an error message, search result, or URL parameter.
- DOM-based XSS — Vulnerability exists in client-side JavaScript that processes untrusted data and writes it to the DOM.
Prevention Checklist
-
✓
Never insert untrusted data into HTML without encoding. Use your framework's built-in escaping (React JSX, Vue templates, Go html/template). Critical
-
✓
Implement Content Security Policy. A strict CSP prevents inline script execution even if an attacker finds an injection point. Critical
-
✓
Use HttpOnly flag on session cookies. Prevents JavaScript from accessing session cookies, mitigating session theft via XSS. Important
-
✓
Sanitize HTML input with DOMPurify. If you must allow some HTML (e.g., rich text editors), use DOMPurify to strip dangerous elements and attributes. Important
-
✓
Avoid innerHTML, document.write, and eval(). These inject raw HTML/JS and are the primary DOM XSS vectors. Important
// 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.
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.
// 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));
# 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})
-
✓
Always use parameterized queries or prepared statements. Never concatenate user input into SQL strings. Critical
-
✓
Use an ORM where possible. ORMs like Prisma, Drizzle, SQLAlchemy, or Django ORM handle parameterization automatically. Recommended
-
✓
Apply least privilege to database users. Your app's database user should only have the permissions it needs. No DROP, no GRANT. Important
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.
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
}));
-
✓
Never use
Access-Control-Allow-Origin: *with credentials. This is actually blocked by browsers, but reflecting arbitrary origins with credentials is equally dangerous. Critical -
✓
Whitelist specific origins. Maintain an explicit list of allowed origins. Do not dynamically reflect the
Originheader. Important -
✓
Restrict allowed methods and headers. Only allow the HTTP methods and headers your API actually uses. Recommended
Authentication Best Practices
Authentication is the gate to your application. A weakness here compromises everything behind it.
-
✓
Implement rate limiting on login endpoints. Prevent brute force attacks by limiting login attempts (e.g., 5 attempts per 15 minutes per IP/account). Critical
-
✓
Use multi-factor authentication (MFA). Offer TOTP (authenticator apps) or passkeys. SMS-based MFA is better than nothing but vulnerable to SIM swapping. Critical
-
✓
Use generic error messages. "Invalid email or password" instead of "User not found" or "Incorrect password". Do not reveal which field is wrong. Important
-
✓
Implement account lockout with progressive delays. After 5 failed attempts, lock for 15 minutes. After 10, lock for 1 hour. Notify the user via email. Important
-
✓
Support passkeys / WebAuthn. Passkeys are phishing-resistant and eliminate password-based attacks entirely. They are the future of authentication. Recommended
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.
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
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
-
✓
Use bcrypt, scrypt, or Argon2id for password hashing. Never MD5, SHA-1, or SHA-256 — they are too fast and can be brute-forced. Critical
-
✓
Enforce minimum password length of 12 characters. Length matters more than complexity rules. Do not require special characters — they lead to predictable patterns like "Password1!". Important
-
✓
Check passwords against breach databases. Use the Have I Been Pwned API (k-Anonymity model) to reject passwords that have appeared in known data breaches. Recommended
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.
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 });
});
});
}
});
-
✓
Set HttpOnly, Secure, and SameSite flags on session cookies. HttpOnly prevents XSS theft. Secure enforces HTTPS. SameSite prevents CSRF. Critical
-
✓
Regenerate session ID after login. Prevents session fixation attacks where an attacker pre-sets a session ID. Important
-
✓
Implement session timeout. Idle sessions should expire. Absolute timeout should also exist (e.g., 24 hours maximum regardless of activity). Important
-
✓
Invalidate sessions on logout. Delete the session from the server-side store. Do not just clear the cookie. Important
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.
# 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 .
-
✓
Run
npm audit/ dependency scanning in CI. Block deployments with critical or high severity vulnerabilities. Critical -
✓
Enable Dependabot or Renovate for automated updates. Get pull requests for dependency updates automatically. Important
-
✓
Use lockfiles and verify integrity. Always commit
package-lock.json/yarn.lock/pnpm-lock.yaml. Usenpm ciin CI (notnpm install). Important -
✓
Minimize dependencies. Every dependency is an attack surface. Question whether you truly need that package, or if a few lines of code would suffice. Recommended
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:
-
✓
Remove all default credentials. Database passwords, admin panels, API keys — change everything from defaults before going live. Critical
-
✓
Store secrets in environment variables or a secrets manager. Never commit secrets to git. Use tools like Vault, AWS Secrets Manager, or Doppler. Critical
-
✓
Disable debug mode in production. Debug pages leak stack traces, environment variables, and internal paths to attackers. Critical
-
✓
Disable directory listing. Web servers should never expose directory contents. Important
-
✓
Set up proper
robots.txt. Prevent search engines from indexing admin panels and sensitive paths. Use NexTool's robots.txt Generator. Recommended -
✓
Remove server version headers. Do not advertise your Nginx/Apache/Node.js version. It helps attackers target known vulnerabilities. Recommended
-
✓
Implement centralized logging and alerting. Log authentication events, permission changes, and errors. Alert on anomalies. Important
-
✓
Set up automated backups with encryption. Test restore procedures regularly. A backup you cannot restore is not a backup. Important
# 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 ToolsSecurity 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.