Back to Blog
SecurityWebXSSCSRF

Web Security Essentials for Developers

Protect your web applications. From XSS to CSRF to security headers and common vulnerabilities.

B
Bootspring Team
Engineering
July 10, 2022
6 min read

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 escaping

Security 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 information

Authentication 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.

Share this article

Help spread the word about Bootspring