Security is critical for web applications. Let's master essential practices to protect your frontend applications from common vulnerabilities.
Common Vulnerabilities
XSS (Cross-Site Scripting)
What: Attackers inject malicious scripts into web pages viewed by other users.
Prevention:
// ❌ BAD: Direct HTML injection
div.innerHTML = userInput;
// ✅ GOOD: Use textContent
div.textContent = userInput;
// ✅ GOOD: Sanitize HTML
import DOMPurify from 'dompurify';
div.innerHTML = DOMPurify.sanitize(userInput);
// ✅ GOOD: Use template literals safely
const element = `<div>${escapeHtml(userInput)}</div>>;
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}CSRF (Cross-Site Request Forgery)
What: Attackers trick users into performing actions they didn't intend.
Prevention:
// Server-side: Generate CSRF token
app.use(session({ secret: 'secret' }));
app.use(csrf());
// Send token to client
app.get('/form', (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// Client-side: Include token in requests
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
credentials: 'same-origin'
});SQL Injection
What: Attackers manipulate database queries through user input.
Prevention:
// ❌ BAD: String concatenation
const query = `SELECT * FROM users WHERE id = '${userId}'`;
// ✅ GOOD: Parameterized queries
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [userId]);
// ✅ GOOD: ORM
const user = await User.findByPk(userId);Content Security Policy (CSP)
CSP restricts what resources can be loaded on your page.
<!-- HTTP Header -->
meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;"
>
<!-- Or via server header -->
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.comCSP Best Practices
// Express.js middleware
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.example.com; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self' data:; " +
"connect-src 'self' https://api.example.com; " +
"frame-ancestors 'none';"
);
next();
});Authentication & Authorization
JWT (JSON Web Tokens)
// Generate token
const jwt = require('jsonwebtoken');
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
// Verify token
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log(decoded.userId); // Access user data
} catch (err) {
// Invalid token
}Secure Password Storage
const bcrypt = require('bcrypt');
const saltRounds = 10;
// Hash password
const hash = await bcrypt.hash(plainPassword, saltRounds);
// Compare password
const match = await bcrypt.compare(plainPassword, hash);Session Management
// Express session
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JavaScript access
sameSite: 'strict', // CSRF protection
maxAge: 3600000 // 1 hour
}
}));Secure Headers
// Helmet.js middleware
const helmet = require('helmet');
app.use(helmet());
// Or manually
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
});Data Protection
Sensitive Data in URLs
// ❌ BAD: Sensitive data in URL
fetch(`/api/user?token=${token}`);
// ✅ GOOD: Use POST with body
fetch('/api/user', {
method: 'POST',
body: JSON.stringify({ token }),
headers: { 'Content-Type': 'application/json' }
});Local Storage Security
// ⚠️ WARNING: localStorage is accessible by JavaScript
// Don't store sensitive data
// ✅ GOOD: Use httpOnly cookies
res.cookie('token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict'
});Environment Variables
// ✅ GOOD: Use environment variables
const dbPassword = process.env.DB_PASSWORD;
// Never commit .env files
// Add to .gitignore
.env
.env.local
.env.productionHTTPS and SSL
Enforce HTTPS
// Redirect HTTP to HTTPS
app.use((req, res, next) => {
if (!req.secure) {
return res.redirect('https://' + req.headers.host + req.url);
}
next();
});
// HSTS header
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');Input Validation
Client-side Validation
// Validate email
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// Sanitize input
function sanitizeInput(input) {
return input.trim().replace(/[<>]/g, '');
}
// Use validation libraries
import Joi from 'joi';
const schema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required()
});
const { error } = schema.validate({ email, password });Server-side Validation
// Always validate on server
app.post('/api/users', (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Missing fields' });
}
if (!isValidEmail(email)) {
return res.status(400).json({ error: 'Invalid email' });
}
if (password.length < 8) {
return res.status(400).json({ error: 'Password too short' });
}
// Create user...
});Error Handling
Safe Error Messages
// ❌ BAD: Expose internal details
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});
// ✅ GOOD: Generic error message
app.use((err, req, res, next) => {
console.error(err); // Log details server-side
res.status(500).json({ error: 'Internal server error' });
});
// ✅ GOOD: Different messages for different environments
const isDev = process.env.NODE_ENV === 'development';
app.use((err, req, res, next) => {
res.status(err.status || 500).json({
error: isDev ? err.message : 'Something went wrong',
...(isDev && { stack: err.stack })
});
});API Security
Rate Limiting
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/api/', limiter);API Key Authentication
// Verify API key
app.use('/api', (req, res, next) => {
const apiKey = req.headers['x-api-key'];
if (!apiKey || apiKey !== process.env.API_KEY) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});Third-party Dependencies
# Check for vulnerabilities
npm audit
# Fix vulnerabilities
npm audit fix
# Review dependencies
npm list
# Update packages
npm updateSecurity Checklist
- [ ] HTTPS enabled
- [ ] Input validation on client and server
- [ ] Output encoding (prevent XSS)
- [ ] CSRF protection
- [ ] Secure password hashing
- [ ] JWT/token security
- [ ] Secure headers configured
- [ ] CSP implemented
- [ ] Dependencies up to date
- [ ] Error handling doesn't expose sensitive info
- [ ] Rate limiting implemented
- [ ] Authentication properly implemented
- [ ] Sensitive data not in localStorage
- [ ] Environment variables used for secrets
Conclusion
Web security is multi-layered. Implement these practices from the start, regularly audit your code, and stay updated on new vulnerabilities. Remember: client-side validation is for UX, server-side validation is for security.