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> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
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
- ✅ HTTPS on all pages
- ✅ CSP headers configured
- ✅ CSRF tokens for forms
- ✅ Parameterized SQL queries
- ✅ Passwords are hashed (bcrypt)
- ✅ Rate limiting on the API
- ✅ Input validation
- ✅ 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.



