Security vulnerabilities can be devastating. Here's how to protect your applications from common attacks.
Cross-Site Scripting (XSS)#
1// XSS happens when user input is rendered as HTML
2
3// ❌ Vulnerable to XSS
4app.get('/search', (req, res) => {
5 const query = req.query.q;
6 res.send(`<h1>Results for: ${query}</h1>`);
7 // If query = "<script>alert('xss')</script>", it executes
8});
9
10// ✓ Escape output
11import { escape } from 'html-escaper';
12
13app.get('/search', (req, res) => {
14 const query = escape(req.query.q);
15 res.send(`<h1>Results for: ${query}</h1>`);
16});
17
18// React escapes by default
19function SearchResults({ query }) {
20 // Safe - React escapes content
21 return <h1>Results for: {query}</h1>;
22}
23
24// ❌ Dangerous - bypasses React's protection
25function DangerousComponent({ html }) {
26 return <div dangerouslySetInnerHTML={{ __html: html }} />;
27}
28
29// ✓ If you must render HTML, sanitize it
30import DOMPurify from 'dompurify';
31
32function SafeHtmlComponent({ html }) {
33 const sanitized = DOMPurify.sanitize(html);
34 return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
35}Cross-Site Request Forgery (CSRF)#
1// CSRF exploits authenticated sessions
2
3// ❌ Vulnerable endpoint
4app.post('/transfer', (req, res) => {
5 const { to, amount } = req.body;
6 // An attacker's page could submit this form
7 transferMoney(req.user.id, to, amount);
8});
9
10// ✓ Use CSRF tokens
11import csrf from 'csurf';
12
13const csrfProtection = csrf({ cookie: true });
14
15app.get('/transfer', csrfProtection, (req, res) => {
16 res.render('transfer', { csrfToken: req.csrfToken() });
17});
18
19app.post('/transfer', csrfProtection, (req, res) => {
20 // Token verified automatically
21 transferMoney(req.user.id, req.body.to, req.body.amount);
22});
23
24// In your form
25<form action="/transfer" method="POST">
26 <input type="hidden" name="_csrf" value="{{csrfToken}}" />
27 <!-- form fields -->
28</form>
29
30// For APIs, use SameSite cookies
31app.use(session({
32 cookie: {
33 httpOnly: true,
34 secure: true,
35 sameSite: 'strict', // Prevents CSRF for most cases
36 },
37}));SQL Injection#
1// ❌ Vulnerable to SQL injection
2const query = `SELECT * FROM users WHERE email = '${email}'`;
3// If email = "' OR '1'='1", returns all users
4
5// ✓ Use parameterized queries
6const result = await db.query(
7 'SELECT * FROM users WHERE email = $1',
8 [email]
9);
10
11// ✓ ORMs handle this automatically
12const user = await prisma.user.findUnique({
13 where: { email },
14});
15
16// ❌ Even with ORMs, raw queries can be vulnerable
17const users = await prisma.$queryRaw`
18 SELECT * FROM users WHERE email = '${email}'
19`; // Still vulnerable!
20
21// ✓ Use proper interpolation
22const users = await prisma.$queryRaw`
23 SELECT * FROM users WHERE email = ${email}
24`; // Prisma handles escapingSecurity Headers#
1import helmet from 'helmet';
2
3const app = express();
4
5// Helmet sets multiple security headers
6app.use(helmet());
7
8// Or configure individually
9app.use(helmet({
10 contentSecurityPolicy: {
11 directives: {
12 defaultSrc: ["'self'"],
13 styleSrc: ["'self'", "'unsafe-inline'"],
14 scriptSrc: ["'self'"],
15 imgSrc: ["'self'", 'data:', 'https:'],
16 },
17 },
18 hsts: {
19 maxAge: 31536000,
20 includeSubDomains: true,
21 preload: true,
22 },
23 referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
24}));
25
26// Key headers explained:
27// Content-Security-Policy: Controls resource loading
28// Strict-Transport-Security: Forces HTTPS
29// X-Content-Type-Options: Prevents MIME sniffing
30// X-Frame-Options: Prevents clickjacking
31// Referrer-Policy: Controls referrer informationAuthentication Security#
1// Password hashing
2import bcrypt from 'bcrypt';
3
4const SALT_ROUNDS = 12;
5
6async function hashPassword(password: string): Promise<string> {
7 return bcrypt.hash(password, SALT_ROUNDS);
8}
9
10async function verifyPassword(
11 password: string,
12 hash: string
13): Promise<boolean> {
14 return bcrypt.compare(password, hash);
15}
16
17// Secure session configuration
18app.use(session({
19 secret: process.env.SESSION_SECRET!, // Long random string
20 name: 'sessionId', // Don't use default 'connect.sid'
21 cookie: {
22 httpOnly: true, // Prevents JavaScript access
23 secure: true, // HTTPS only
24 sameSite: 'lax', // CSRF protection
25 maxAge: 24 * 60 * 60 * 1000, // 24 hours
26 },
27 resave: false,
28 saveUninitialized: false,
29}));
30
31// Rate limiting for auth endpoints
32import rateLimit from 'express-rate-limit';
33
34const authLimiter = rateLimit({
35 windowMs: 15 * 60 * 1000, // 15 minutes
36 max: 5, // 5 attempts
37 message: 'Too many login attempts',
38});
39
40app.post('/login', authLimiter, async (req, res) => {
41 // Login logic
42});Input Validation#
1import { z } from 'zod';
2
3// Define schema
4const userSchema = z.object({
5 email: z.string().email().max(255),
6 password: z.string().min(8).max(100),
7 age: z.number().int().min(0).max(150).optional(),
8});
9
10// Validate input
11app.post('/users', (req, res) => {
12 const result = userSchema.safeParse(req.body);
13
14 if (!result.success) {
15 return res.status(400).json({
16 error: 'Validation failed',
17 details: result.error.flatten(),
18 });
19 }
20
21 // result.data is typed and validated
22 createUser(result.data);
23});
24
25// Sanitize output
26function sanitizeUser(user: User): PublicUser {
27 const { password, ...publicUser } = user;
28 return publicUser;
29}File Upload Security#
1import multer from 'multer';
2import path from 'path';
3
4// Validate file type
5const upload = multer({
6 storage: multer.diskStorage({
7 destination: './uploads',
8 filename: (req, file, cb) => {
9 // Generate safe filename
10 const uniqueSuffix = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
11 cb(null, `${uniqueSuffix}${path.extname(file.originalname)}`);
12 },
13 }),
14 limits: {
15 fileSize: 5 * 1024 * 1024, // 5MB
16 },
17 fileFilter: (req, file, cb) => {
18 const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
19
20 if (!allowedTypes.includes(file.mimetype)) {
21 cb(new Error('Invalid file type'));
22 return;
23 }
24
25 // Also check magic bytes for extra security
26 cb(null, true);
27 },
28});
29
30app.post('/upload', upload.single('file'), (req, res) => {
31 // File is validated and saved
32});Secrets Management#
1// ❌ Never hardcode secrets
2const apiKey = 'sk_live_abc123';
3
4// ✓ Use environment variables
5const apiKey = process.env.API_KEY;
6
7// ✓ Validate required env vars at startup
8function validateEnv(): void {
9 const required = ['DATABASE_URL', 'JWT_SECRET', 'API_KEY'];
10 const missing = required.filter(key => !process.env[key]);
11
12 if (missing.length > 0) {
13 throw new Error(`Missing env vars: ${missing.join(', ')}`);
14 }
15}
16
17// ✓ Use secrets manager in production
18import { SecretsManager } from '@aws-sdk/client-secrets-manager';
19
20async function getSecret(name: string): Promise<string> {
21 const client = new SecretsManager();
22 const response = await client.getSecretValue({ SecretId: name });
23 return response.SecretString!;
24}Security Checklist#
Authentication:
✓ Hash passwords with bcrypt/argon2
✓ Implement rate limiting
✓ Use secure session settings
✓ Implement MFA where appropriate
Data:
✓ Validate all input
✓ Escape all output
✓ Use parameterized queries
✓ Sanitize file uploads
Transport:
✓ Use HTTPS everywhere
✓ Set security headers
✓ Configure CORS properly
✓ Use secure cookies
Secrets:
✓ Never commit secrets
✓ Use environment variables
✓ Rotate credentials regularly
✓ Use secrets manager in production
Conclusion#
Security requires defense in depth. Validate input, escape output, use parameterized queries, set security headers, and manage secrets properly. Regular security audits and staying updated on vulnerabilities are essential for maintaining secure applications.