Web Security Fundamentals
Learn essential web security practices to protect your JavaScript applications and users.
Why Web Security Matters
Web security protects your application, data, and users from malicious attacks. A single vulnerability can lead to data breaches, financial loss, and damage to your reputation.
Common Web Security Threats
Cross-Site Scripting (XSS)
XSS attacks inject malicious scripts into web pages viewed by other users.
Types of XSS:
- Stored XSS: Malicious script stored in database
- Reflected XSS: Script reflected in server response
- DOM-based XSS: Script executes in browser DOM
Prevention:
// Bad - vulnerable to XSS
element.innerHTML = userInput;
// Good - escape HTML
function escapeHtml(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, m => map[m]);
}
element.textContent = userInput;
// Better - use textContent instead of innerHTML
element.textContent = userInput;
// Best - use DOMPurify library
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);SQL Injection
SQL injection occurs when malicious SQL code is inserted into user input.
Prevention with Parameterized Queries:
// Bad - vulnerable to SQL injection
const query = `SELECT * FROM users WHERE email = '${email}'`;
// Good - parameterized query
const query = 'SELECT * FROM users WHERE email = ?';
db.query(query, [email]);
// Using ORM (like Sequelize)
const user = await User.findOne({ where: { email } });Cross-Site Request Forgery (CSRF)
CSRF tricks authenticated users into performing unwanted actions.
Prevention with CSRF Tokens:
// Express.js with csurf middleware
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/api/user', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// Include token in forms
app.post('/api/update', csrfProtection, (req, res) => {
// Protected route
});Authentication & Authorization
Password Security
Hashing Passwords:
const bcrypt = require('bcrypt');
// Hash password
async function hashPassword(password) {
const saltRounds = 10;
return await bcrypt.hash(password, saltRounds);
}
// Verify password
async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
// Usage
const hashedPassword = await hashPassword(userPassword);
const isValid = await verifyPassword(inputPassword, storedHash);JWT Authentication
const jwt = require('jsonwebtoken');
// Generate token
function generateToken(user) {
return jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
}
// Verify token
function verifyToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
throw new Error('Invalid token');
}
}
// Middleware for protected routes
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
try {
const decoded = verifyToken(token);
req.user = decoded;
next();
} catch (error) {
return res.status(403).json({ error: 'Invalid token' });
}
}Session Management
const expressSession = require('express-session');
app.use(expressSession({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only
httpOnly: true, // Prevent XSS
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));Input Validation
Server-Side Validation
const Joi = require('joi');
// Validation schema
const userSchema = Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/).required(),
age: Joi.number().integer().min(13).max(120)
});
// Validation middleware
function validateUser(req, res, next) {
const { error } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({
error: 'Validation failed',
details: error.details
});
}
next();
}
// Usage
app.post('/api/users', validateUser, (req, res) => {
// Process valid data
});Client-Side Validation
// Email validation
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// Password strength validation
function isStrongPassword(password) {
const minLength = 8;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
return password.length >= minLength &&
hasUpperCase &&
hasLowerCase &&
hasNumbers &&
hasSpecialChar;
}Secure Headers
Using Helmet.js
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));Important Security Headers
| Header | Purpose |
|---|---|
X-Frame-Options | Prevent clickjacking |
X-Content-Type-Options | Prevent MIME sniffing |
X-XSS-Protection | Enable XSS filtering |
Strict-Transport-Security | Enforce HTTPS |
Content-Security-Policy | Control resource loading |
HTTPS and SSL
Enforcing HTTPS
// Force HTTPS in production
function requireHTTPS(req, res, next) {
if (!req.secure && req.get('x-forwarded-proto') !== 'https') {
return res.redirect(301, `https://${req.get('host')}${req.url}`);
}
next();
}
if (process.env.NODE_ENV === 'production') {
app.use(requireHTTPS);
}SSL Configuration
const https = require('https');
const fs = require('fs');
// For development only
const options = {
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.cert')
};
https.createServer(options, app).listen(443);Data Protection
Encrypting Sensitive Data
const crypto = require('crypto');
// Encryption
function encrypt(text, secretKey) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipher('aes-256-cbc', secretKey);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
// Decryption
function decrypt(text, secretKey) {
const textParts = text.split(':');
const iv = Buffer.from(textParts.shift(), 'hex');
const encryptedText = textParts.join(':');
const decipher = crypto.createDecipher('aes-256-cbc', secretKey);
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}Environment Variables
// Use dotenv for environment variables
require('dotenv').config();
// Store secrets in .env file
// DB_PASSWORD=your_secret_password
// JWT_SECRET=your_jwt_secret
// API_KEY=your_api_key
// Access in code
const dbPassword = process.env.DB_PASSWORD;
const jwtSecret = process.env.JWT_SECRET;Rate Limiting
Preventing Brute Force Attacks
const rateLimit = require('express-rate-limit');
// General rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per window
message: 'Too many requests from this IP'
});
app.use(limiter);
// Stricter rate limiting for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 5 attempts per 15 minutes
skipSuccessfulRequests: true
});
app.post('/api/login', authLimiter, (req, res) => {
// Login logic
});Security Best Practices Checklist
Development
- Use parameterized queries for database operations
- Validate all user input (server-side)
- Escape output to prevent XSS
- Use HTTPS in production
- Implement proper authentication
- Add rate limiting to prevent abuse
Production
- Set secure HTTP headers
- Enable CORS properly
- Use environment variables for secrets
- Implement logging and monitoring
- Regular security audits
- Keep dependencies updated
Code Review
- Check for SQL injection vulnerabilities
- Verify XSS protection
- Ensure proper authentication
- Validate authorization checks
- Review error handling (don’t leak info)
Security Testing
Using Security Linters
# Install security linter
npm install -g eslint-plugin-security
# Run security checks
eslint --plugin security src/Dependency Security
# Check for vulnerable dependencies
npm audit
# Fix vulnerabilities
npm audit fixResources
- OWASP Top 10 - Most critical web security risks
- MDN Web Security - Browser security features
- Node.js Security Best Practices
- Helmet.js Documentation - Security middleware for Express
Next Steps
Security is an ongoing process. Stay updated with the latest security practices and regularly audit your applications for vulnerabilities.
Last updated on