Web Security Basics Every Developer Must Know
Your app works. But is it safe? Here's what you need to know.
1. SQL Injection
// ❌ Vulnerable — attacker can inject SQL
const query = `SELECT * FROM users WHERE id = ${userId}`;
db.query(query);
// Input: userId = "1 OR 1=1" → Returns ALL users!
// Input: userId = "1; DROP TABLE users;" → Data deleted!
// ✅ Safe — parameterized queries
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [userId]);
// Or with an ORM (even safer)
const user = await User.findByPk(userId);
2. XSS (Cross-Site Scripting)
// ❌ Vulnerable — renders untrusted HTML
div.innerHTML = userComment;
// Attacker input: <script>stealCookies()</script>
// or: <img src=x onerror="stealCookies()">
// ✅ Safe — escape all output
div.textContent = userComment; // Text only, no HTML parsing
div.innerText = userComment; // Same as textContent
// If you NEED HTML (rich text editor):
import DOMPurify from 'dompurify';
div.innerHTML = DOMPurify.sanitize(userComment); // Removes dangerous tags/attributes
// In React/Vue/Angular, JSX templates auto-escape:
// <div>{userComment}</div> // Safe by default!
// dangerouslySetInnerHTML={{ __html: userComment }} // DANGEROUS! Only with sanitization
3. CSRF (Cross-Site Request Forgery)
// Attack: User visits evil.com while logged into your site
// evil.com has: <img src="https://your-site.com/transfer?to=attacker&amount=1000">
// Browser automatically sends cookies → transfer executed!
// Defense: CSRF tokens + SameSite cookies
// Express + csurf middleware:
import csrf from 'csurf';
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/transfer', csrfProtection, (req, res) => {
// Token validated automatically
processTransfer(req.body);
});
// Cookie defense:
app.use(session({
cookie: {
sameSite: 'strict', // Don't send cookies to other sites
secure: true, // Only over HTTPS
httpOnly: true, // JavaScript can't read the cookie
}
}));
4. Authentication Security
// ❌ Storing passwords in plain text
user.password = 'password123'; // NEVER!
// ✅ Hashing with bcrypt
import bcrypt from 'bcryptjs';
const SALT_ROUNDS = 12;
// Register
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
await User.create({ email, password: hashedPassword });
// Login
const user = await User.findOne({ where: { email } });
if (!user) return error('User not found');
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) return error('Invalid password');
// JWT tokens (stateless auth)
import jwt from 'jsonwebtoken';
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
// Validate token
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.user = payload; // Attach to request
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
5. Rate Limiting
// Without rate limiting:
// - Brute force login attempts
// - API abuse / scraping
// - DDoS amplification
import rateLimit from 'express-rate-limit';
// General API limiter
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per IP
message: { error: 'Too many requests' },
standardHeaders: true,
});
app.use('/api/', apiLimiter);
// Stricter for login endpoint
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // Only 5 attempts per 15 min
message: { error: 'Too many login attempts. Try again later.' },
skipSuccessfulRequests: true, // Don't count successful logins
});
app.post('/api/login', loginLimiter, handleLogin);
6. Input Validation
import { z } from 'zod';
const registerSchema = z.object({
email: z.string().email('Invalid email').max(255),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain uppercase letter')
.regex(/[a-z]/, 'Must contain lowercase letter')
.regex(/[0-9]/, 'Must contain a number'),
name: z.string().min(1).max(100).trim(),
age: z.number().int().min(18).max(150).optional(),
});
// Validate before processing
function register(req, res) {
const result = registerSchema.safeParse(req.body);
if (!result.success) {
return res.status(422).json({
error: 'Validation failed',
details: result.error.flatten()
});
}
// Process valid data...
}
7. HTTP Security Headers
import helmet from 'helmet';
app.use(helmet());
// What helmet adds:
// X-Content-Type-Options: nosniff // Prevent MIME sniffing
// X-Frame-Options: SAMEORIGIN // Prevent clickjacking
// X-XSS-Protection: 0 // Legacy XSS filter
// Referrer-Policy: strict-origin-when-cross-origin
// Permissions-Policy: ... // Control browser features
// Content-Security-Policy: ... // Control resource loading
// Custom CSP (most powerful header):
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // Remove unsafe-inline in production!
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
connectSrc: ["'self'", "https://api.example.com"],
frameAncestors: ["'none'"],
},
}));
8. CORS Configuration
import cors from 'cors';
// ❌ Too permissive
app.use(cors()); // Allows any origin!
// ✅ Proper configuration
app.use(cors({
origin: function (origin, callback) {
const allowedOrigins = [
'https://myapp.com',
'https://www.myapp.com',
];
// Allow in development
if (process.env.NODE_ENV === 'development') {
return callback(null, true);
}
if (!origin) return callback(null, true); // Mobile apps, Postman
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true, // Allow cookies/auth headers
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400, // Preflight cache for 24 hours
}));
Security Checklist
Before deploying to production:
Authentication & Authz
[ ] Passwords hashed (bcrypt/argon2)
[ ] JWT secrets are strong (32+ random chars)
[ ] Tokens have reasonable expiry
[ ] Role-based access control implemented
[ ] Sessions use secure, httpOnly, sameSite cookies
Input Handling
[ ] All inputs validated (server-side!)
[ ] SQL uses parameterized queries / ORM
[ ] Output is escaped/sanitized
[ ] File uploads validate type AND content
[ ] Rate limiting on all public endpoints
HTTP Security
[ ] Helmet installed and configured
[ ] CSP header set appropriately
[ ] CORS restricted to known origins
[ ] HTTPS enforced (redirect HTTP)
[ ] HSTS enabled
Data Protection
[ ] No sensitive data in URLs or logs
[ ] PII encrypted at rest
[ ] API keys not exposed to client
[ ] Error messages don't leak internals
[ ] Secrets in env vars (never in code!)
Infrastructure
[ ] Dependencies up to date (npm audit)
[ ] Environment variables configured correctly
[ ] Firewall rules restrictive
[ ] Access logs enabled and monitored
[ ] Backup strategy in place
What's the most important security lesson you've learned?
Follow @armorbreak for more web development content.
Top comments (0)