Comprehensive web security guide covering XSS prevention, CSRF protection, SQL injection defense, Content Security Policy, and security headers for modern web apps.
Web Security: Complete Prevention Guide
XSS Prevention
// React - already HTML-escapes by default
// Dangerous: dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: userInput }} /> // NEVER do this with untrusted input
// Safe: Sanitize before using dangerouslySetInnerHTML
import DOMPurify from 'dompurify'
function SafeHTML({ content }) {
const clean = DOMPurify.sanitize(content, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
ALLOWED_ATTR: [],
})
return <div dangerouslySetInnerHTML={{ __html: clean }} />
}
// Server-side: escape all user content
import escapeHtml from 'escape-html'
app.get('/profile', (req, res) => {
const username = escapeHtml(req.query.username)
res.send(`<h1>Welcome, ${username}!</h1>`)
})
// CSP header to block inline scripts
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' 'nonce-{RANDOM}'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"connect-src 'self' https://api.example.com"
)
next()
})
CSRF Protection
import csrf from 'csurf'
import cookieParser from 'cookie-parser'
app.use(cookieParser())
app.use(csrf({ cookie: { httpOnly: true, secure: true, sameSite: 'strict' } }))
// Include CSRF token in forms
app.get('/form', (req, res) => {
res.render('form', { csrfToken: req.csrfToken() })
})
// Frontend: include token in AJAX requests
const token = document.querySelector('meta[name="csrf-token"]').content
fetch('/api/action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token,
},
body: JSON.stringify({ data: 'value' }),
})
// Double Submit Cookie Pattern for SPAs
function getCSRFToken() {
return document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1]
}
// SameSite cookies provide additional protection
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'strict', // Prevents cross-site requests
maxAge: 3600000,
})
SQL Injection Prevention
// NEVER: string concatenation
const query = `SELECT * FROM users WHERE id = ${userId}` // VULNERABLE
// ALWAYS: parameterized queries
import { Pool } from 'pg'
const pool = new Pool()
async function getUser(userId) {
const result = await pool.query(
'SELECT id, name, email FROM users WHERE id = $1',
[userId] // Parameterized - safe
)
return result.rows[0]
}
// With Prisma ORM - safe by default
const user = await prisma.user.findUnique({ where: { id: userId } })
// Input validation with Zod
import { z } from 'zod'
const UserIdSchema = z.string().uuid()
const SearchSchema = z.object({
query: z.string().max(100).regex(/^[a-zA-Z0-9 ]+$/),
page: z.number().int().positive().max(1000),
})
app.get('/search', async (req, res) => {
const { query, page } = SearchSchema.parse(req.query)
const results = await searchProducts(query, page)
res.json(results)
})
Security Headers with Helmet.js
import helmet from 'helmet'
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'nonce-abc123'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
upgradeInsecureRequests: [],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
frameguard: { action: 'deny' },
noSniff: true,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}))
// Rate limiting
import rateLimit from 'express-rate-limit'
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests, please try again later.' },
})
app.use('/api/', limiter)
const loginLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5,
skipSuccessfulRequests: true,
})
app.use('/api/auth/login', loginLimiter)
Input Validation and Sanitization
import { z } from 'zod'
import validator from 'validator'
const RegisterSchema = z.object({
email: z.string().email().toLowerCase().trim(),
password: z.string()
.min(12, 'Password must be at least 12 characters')
.regex(/[A-Z]/, 'Must contain uppercase')
.regex(/[0-9]/, 'Must contain number')
.regex(/[^A-Za-z0-9]/, 'Must contain special character'),
username: z.string()
.min(3).max(30)
.regex(/^[a-zA-Z0-9_-]+$/, 'Only letters, numbers, hyphens, underscores'),
})
// File upload validation
const FileSchema = z.object({
mimetype: z.enum(['image/jpeg', 'image/png', 'image/webp']),
size: z.number().max(5 * 1024 * 1024), // 5MB max
})
function validateFile(file) {
// Check actual content, not just MIME type
const magic = file.buffer.slice(0, 4)
const isPNG = magic.equals(Buffer.from([0x89, 0x50, 0x4e, 0x47]))
const isJPEG = magic[0] === 0xff && magic[1] === 0xd8
return isPNG || isJPEG
}
Security Checklist
| Category |
Controls |
| XSS |
CSP, output encoding, DOMPurify |
| CSRF |
SameSite cookies, CSRF tokens |
| Injection |
Parameterized queries, input validation |
| Headers |
Helmet.js, HSTS, X-Frame-Options |
| Auth |
Rate limiting, bcrypt, MFA |