Web Security Fundamentals: Protecting Your Applications
Web security is not optional in today's threat landscape. Every web application is a potential target, and security must be built in from the ground up. Let's explore the fundamental principles and practical techniques for securing web applications.
OWASP Top 10 Vulnerabilities
1. Injection Attacks
SQL injection remains one of the most dangerous vulnerabilities:
// BAD: Vulnerable to SQL injection
app.post('/login', (req, res) => {
const { username, password } = req.body;
const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
db.query(query, (err, results) => {
// Vulnerable to: admin' --
if (results.length > 0) {
res.json({ success: true });
}
});
});
// GOOD: Using parameterized queries
app.post('/login', async (req, res) => {
const { username, password } = req.body;
try {
// Input validation
if (!username || !password) {
return res.status(400).json({ error: 'Username and password required' });
}
// Parameterized query
const query = 'SELECT id, username, password_hash FROM users WHERE username = ?';
const [rows] = await db.execute(query, [username]);
if (rows.length === 0) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const user = rows[0];
const isValid = await bcrypt.compare(password, user.password_hash);
if (isValid) {
const token = jwt.sign(
{ userId: user.id, username: user.username },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ token });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
2. Cross-Site Scripting (XSS)
Prevent XSS attacks through proper output encoding:
// Input sanitization middleware
const xss = require('xss');
const DOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
const purify = DOMPurify(window);
function sanitizeInput(req, res, next) {
if (req.body) {
for (const key in req.body) {
if (typeof req.body[key] === 'string') {
// Basic XSS filtering
req.body[key] = xss(req.body[key], {
whiteList: {
p: [],
br: [],
strong: [],
em: [],
u: []
},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script']
});
}
}
}
next();
}
// Content Security Policy
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; " +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
"font-src 'self' https://fonts.gstatic.com; " +
"img-src 'self' data: https:; " +
"connect-src 'self' https://api.example.com"
);
next();
});
// Template rendering with automatic escaping
app.set('view engine', 'ejs');
app.locals.escapeHtml = (unsafe) => {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
};
3. Cross-Site Request Forgery (CSRF)
Implement CSRF protection:
const csrf = require('csurf');
// CSRF protection middleware
const csrfProtection = csrf({
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
}
});
app.use(csrfProtection);
// Make CSRF token available to templates
app.use((req, res, next) => {
res.locals.csrfToken = req.csrfToken();
next();
});
// In your forms
// <input type="hidden" name="_csrf" value="<%= csrfToken %>">
// For AJAX requests
app.get('/csrf-token', (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// Double Submit Cookie pattern for SPAs
function generateCSRFToken() {
return crypto.randomBytes(32).toString('hex');
}
app.use('/api', (req, res, next) => {
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
return next();
}
const tokenFromHeader = req.headers['x-csrf-token'];
const tokenFromCookie = req.cookies['csrf-token'];
if (!tokenFromHeader || !tokenFromCookie || tokenFromHeader !== tokenFromCookie) {
return res.status(403).json({ error: 'CSRF token mismatch' });
}
next();
});
Authentication and Session Management
Secure Password Handling
const bcrypt = require('bcrypt');
const zxcvbn = require('zxcvbn');
// Password validation
function validatePassword(password) {
const result = zxcvbn(password);
if (result.score < 3) {
return {
valid: false,
message: 'Password is too weak',
suggestions: result.feedback.suggestions
};
}
if (password.length < 12) {
return {
valid: false,
message: 'Password must be at least 12 characters long'
};
}
return { valid: true };
}
// Secure password hashing
async function hashPassword(password) {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
}
// User registration with security checks
app.post('/register', async (req, res) => {
const { username, email, password } = req.body;
try {
// Validate input
if (!username || !email || !password) {
return res.status(400).json({ error: 'All fields required' });
}
// Email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(400).json({ error: 'Invalid email format' });
}
// Password strength check
const passwordValidation = validatePassword(password);
if (!passwordValidation.valid) {
return res.status(400).json({
error: passwordValidation.message,
suggestions: passwordValidation.suggestions
});
}
// Check if user already exists
const existingUser = await User.findOne({
$or: [{ email }, { username }]
});
if (existingUser) {
return res.status(409).json({ error: 'User already exists' });
}
// Hash password
const passwordHash = await hashPassword(password);
// Create user
const user = new User({
username,
email,
passwordHash,
emailVerified: false,
createdAt: new Date()
});
await user.save();
// Send verification email
await sendVerificationEmail(user);
res.status(201).json({
message: 'User created successfully. Please verify your email.'
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
JWT Token Security
const jwt = require('jsonwebtoken');
const redis = require('redis');
const client = redis.createClient();
// Secure JWT configuration
const JWT_CONFIG = {
accessTokenExpiry: '15m',
refreshTokenExpiry: '7d',
algorithm: 'HS256'
};
// Generate token pair
function generateTokens(payload) {
const accessToken = jwt.sign(
payload,
process.env.JWT_ACCESS_SECRET,
{
expiresIn: JWT_CONFIG.accessTokenExpiry,
algorithm: JWT_CONFIG.algorithm
}
);
const refreshToken = jwt.sign(
{ userId: payload.userId },
process.env.JWT_REFRESH_SECRET,
{
expiresIn: JWT_CONFIG.refreshTokenExpiry,
algorithm: JWT_CONFIG.algorithm
}
);
return { accessToken, refreshToken };
}
// Store refresh token in Redis
async function storeRefreshToken(userId, refreshToken) {
const tokenId = crypto.randomBytes(16).toString('hex');
await client.setex(
`refresh_token:${userId}:${tokenId}`,
7 * 24 * 60 * 60, // 7 days in seconds
refreshToken
);
return tokenId;
}
// JWT middleware with blacklist checking
async 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 {
// Check if token is blacklisted
const isBlacklisted = await client.get(`blacklist:${token}`);
if (isBlacklisted) {
return res.status(401).json({ error: 'Token has been revoked' });
}
const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(403).json({ error: 'Invalid token' });
}
}
// Token refresh endpoint
app.post('/refresh-token', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
try {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
// Check if refresh token exists in Redis
const storedTokens = await client.keys(`refresh_token:${decoded.userId}:*`);
let tokenValid = false;
for (const key of storedTokens) {
const storedToken = await client.get(key);
if (storedToken === refreshToken) {
tokenValid = true;
break;
}
}
if (!tokenValid) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
// Generate new tokens
const user = await User.findById(decoded.userId);
const tokens = generateTokens({
userId: user.id,
username: user.username,
role: user.role
});
// Store new refresh token
const tokenId = await storeRefreshToken(user.id, tokens.refreshToken);
res.json({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken
});
} catch (error) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
Input Validation and Sanitization
const Joi = require('joi');
const validator = require('validator');
// Comprehensive validation schemas
const userSchema = Joi.object({
username: Joi.string()
.alphanum()
.min(3)
.max(30)
.required()
.messages({
'string.alphanum': 'Username can only contain letters and numbers',
'string.min': 'Username must be at least 3 characters long',
'string.max': 'Username cannot exceed 30 characters'
}),
email: Joi.string()
.email({ minDomainSegments: 2 })
.required()
.messages({
'string.email': 'Please provide a valid email address'
}),
password: Joi.string()
.min(12)
.pattern(new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])'))
.required()
.messages({
'string.min': 'Password must be at least 12 characters long',
'string.pattern.base': 'Password must contain uppercase, lowercase, number, and special character'
}),
age: Joi.number()
.integer()
.min(13)
.max(120)
.required(),
website: Joi.string()
.uri()
.optional(),
bio: Joi.string()
.max(500)
.optional()
});
// Validation middleware
function validateInput(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true
});
if (error) {
const errors = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return res.status(400).json({
error: 'Validation failed',
details: errors
});
}
req.body = value;
next();
};
}
// Advanced sanitization
function sanitizeInput(input, type = 'text') {
if (typeof input !== 'string') return input;
switch (type) {
case 'email':
return validator.normalizeEmail(input.trim().toLowerCase());
case 'url':
return validator.isURL(input) ? input : null;
case 'html':
return purify.sanitize(input, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
});
case 'filename':
return input.replace(/[^a-zA-Z0-9._-]/g, '').substring(0, 100);
default:
return input.trim();
}
}
// File upload security
const multer = require('multer');
const path = require('path');
const upload = multer({
dest: 'uploads/',
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
files: 1
},
fileFilter: (req, file, cb) => {
// Check file type
const allowedTypes = /jpeg|jpg|png|gif|pdf|doc|docx/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (mimetype && extname) {
return cb(null, true);
} else {
cb(new Error('Invalid file type'));
}
}
});
// Secure file upload endpoint
app.post('/upload', upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Additional security checks
const fileBuffer = fs.readFileSync(req.file.path);
// Check for malicious content
if (fileBuffer.includes('<?php') || fileBuffer.includes('<script>')) {
fs.unlinkSync(req.file.path);
return res.status(400).json({ error: 'Malicious content detected' });
}
// Generate secure filename
const fileExtension = path.extname(req.file.originalname);
const secureFilename = `${crypto.randomBytes(16).toString('hex')}${fileExtension}`;
const finalPath = path.join('uploads', secureFilename);
fs.renameSync(req.file.path, finalPath);
res.json({
message: 'File uploaded successfully',
filename: secureFilename
});
} catch (error) {
console.error('Upload error:', error);
res.status(500).json({ error: 'Upload failed' });
}
});
Rate Limiting and DDoS Protection
const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');
// Basic rate limiting
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: {
error: 'Too many requests from this IP, please try again later'
},
standardHeaders: true,
legacyHeaders: false,
// Store in Redis for distributed systems
store: new RedisStore({
sendCommand: (...args) => client.sendCommand(args),
}),
});
// Strict rate limiting for authentication endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: {
error: 'Too many login attempts, please try again later'
},
skipSuccessfulRequests: true,
});
// Progressive delay for repeated requests
const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000,
delayAfter: 10,
delayMs: 500,
maxDelayMs: 20000,
});
// IP-based blocking for malicious behavior
const suspiciousIPs = new Set();
function blockSuspiciousIPs(req, res, next) {
const ip = req.ip;
if (suspiciousIPs.has(ip)) {
return res.status(403).json({ error: 'IP blocked due to suspicious activity' });
}
next();
}
// Monitor for suspicious patterns
function detectSuspiciousActivity(req, res, next) {
const ip = req.ip;
const userAgent = req.get('User-Agent');
// Check for common attack patterns
if (!userAgent || userAgent.length < 10) {
suspiciousIPs.add(ip);
return res.status(403).json({ error: 'Suspicious request detected' });
}
// Check for SQL injection patterns in any parameter
const allParams = { ...req.query, ...req.body, ...req.params };
for (const [key, value] of Object.entries(allParams)) {
if (typeof value === 'string') {
const sqlInjectionPattern = /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER)\b)|(--|\/\*|\*\/|;|'|")/i;
if (sqlInjectionPattern.test(value)) {
suspiciousIPs.add(ip);
return res.status(403).json({ error: 'Malicious input detected' });
}
}
}
next();
}
// Apply security middleware
app.use(generalLimiter);
app.use(speedLimiter);
app.use(blockSuspiciousIPs);
app.use(detectSuspiciousActivity);
app.use('/auth', authLimiter);
HTTPS and Security Headers
const helmet = require('helmet');
// Comprehensive security headers
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "https:"],
scriptSrc: ["'self'"],
connectSrc: ["'self'", "https://api.example.com"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
// Additional security headers
app.use((req, res, next) => {
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// Prevent MIME sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// XSS protection
res.setHeader('X-XSS-Protection', '1; mode=block');
// Referrer policy
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// Permissions policy
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
next();
});
// Force HTTPS in production
if (process.env.NODE_ENV === 'production') {
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
res.redirect(`https://${req.header('host')}${req.url}`);
} else {
next();
}
});
}
Security Best Practices Checklist
- Input Validation: Validate and sanitize all input data
- Output Encoding: Encode output to prevent XSS
- Authentication: Implement strong authentication mechanisms
- Authorization: Use proper access controls
- HTTPS: Encrypt all data in transit
- Secure Headers: Implement comprehensive security headers
- Rate Limiting: Protect against brute force attacks
- Error Handling: Don't expose sensitive information in errors
- Logging: Log security events for monitoring
- Dependencies: Keep dependencies updated and secure
- Secrets Management: Never hardcode secrets
- Regular Testing: Perform security audits and penetration testing
Conclusion
Web security is an ongoing process, not a one-time implementation. Stay informed about the latest threats, regularly update your dependencies, and always follow the principle of defense in depth. Security should be considered at every stage of development, from design to deployment and maintenance.
Remember: it's better to be overly cautious with security than to deal with the consequences of a breach.