Web Security Essentials — Protecting Your Applications
Introduction: Security is Everyone’s Responsibility
Web security isn’t just a technical concern—it’s a business imperative. Data breaches cost organizations millions of dollars in recovery, legal fees, and reputation damage. More importantly, they harm users whose personal information is compromised. Every developer bears responsibility for building secure applications.
This comprehensive guide covers essential security principles, common vulnerabilities, and practical implementation strategies.
Table of Contents
- Why Security Matters
- OWASP Top 10
- Injection Attacks
- Cross-Site Scripting (XSS)
- Cross-Site Request Forgery (CSRF)
- Authentication & Authorization
- Encryption & Hashing
- Secure Communication
- API Security
- Security Headers
- Dependency Management
- Best Practices
- Conclusion
Why Security Matters
- Data Protection: Safeguard user privacy
- Business Continuity: Prevent costly breaches
- Compliance: Meet regulatory requirements (GDPR, HIPAA, etc.)
- Reputation: Maintain user trust
- Legal: Avoid liability and lawsuits
OWASP Top 10 (2021)
The Open Web Application Security Project (OWASP) identifies the most critical web security risks. Understanding these vulnerabilities is essential for building secure applications.
1. Broken Access Control
Users can act outside their intended permissions. This is the most common and critical vulnerability.
// ❌ Bad - No access control
router.get('/admin/dashboard', (req, res) => {
res.json(sensitiveData);
});
// ✅ Good - Check authorization
router.get('/admin/dashboard', (req, res) => {
if (req.user.role !== 'ADMIN') {
return res.status(403).json({ error: 'Unauthorized' });
}
res.json(sensitiveData);
});
// Better - Use middleware
function requireRole(role) {
return (req, res, next) => {
if (req.user?.role !== role) {
return res.status(403).json({ error: 'Unauthorized' });
}
next();
};
}
router.get('/admin/dashboard', requireRole('ADMIN'), (req, res) => {
res.json(sensitiveData);
});
2. Cryptographic Failures
Sensitive data exposed due to poor encryption practices. Always encrypt sensitive data in transit and at rest.
// ❌ Bad - Storing plaintext passwords
db.users.create({ email, password }); // NO!
// ✅ Good - Hash passwords with bcrypt
const bcrypt = require('bcrypt');
async function registerUser(email, password) {
const hashedPassword = await bcrypt.hash(password, 10);
db.users.create({ email, hashedPassword });
}
async function loginUser(email, password) {
const user = await db.users.findOne({ email });
const isValid = await bcrypt.compare(password, user.hashedPassword);
if (!isValid) {
throw new Error('Invalid credentials');
}
return user;
}
3. Injection
Untrusted data sent to interpreter as part of command. This includes SQL, NoSQL, OS command, and LDAP injection.
Injection Attacks
SQL Injection
// ❌ Bad - Concatenating user input
const query = `SELECT * FROM users WHERE email = '${email}'`;
db.query(query);
// ✅ Good - Use parameterized queries
const query = 'SELECT * FROM users WHERE email = ?';
db.query(query, [email]);
// With ORM (Prisma)
const user = await prisma.user.findUnique({
where: { email }
});
NoSQL Injection
NoSQL databases are vulnerable to injection if queries accept raw user input. Always validate and sanitize input.
// ❌ Bad - Direct query object manipulation
const user = await User.findOne({ email: req.body.email });
// ✅ Good - Validate and sanitize input
const email = String(req.body.email).trim();
const user = await User.findOne({ email });
// Even better - Use schema validation
import { z } from 'zod';
const emailSchema = z.string().email();
const email = emailSchema.parse(req.body.email);
const user = await User.findOne({ email });
OS Command Injection
// ❌ Bad - Using shell commands with user input
const { execSync } = require('child_process');
execSync(`convert ${imagePath} thumbnail.jpg`);
// ✅ Good - Use libraries instead of shell
const ImageMagick = require('imagemagick');
ImageMagick.convert([imagePath, 'thumbnail.jpg']);
// Or use safe APIs with argument arrays
const { spawn } = require('child_process');
spawn('convert', [imagePath, 'thumbnail.jpg']);
Cross-Site Scripting (XSS)
Injecting malicious scripts into web pages viewed by other users. XSS can steal cookies, hijack sessions, or deface content.
Stored XSS Prevention
// ❌ Bad - Rendering unsanitized HTML
app.get('/posts/:id', (req, res) => {
const post = db.posts.find(req.params.id);
res.send(`<h1>${post.title}</h1><p>${post.content}</p>`);
});
// ✅ Good - Escape HTML
const { escapeHtml } = require('escape-html');
app.get('/posts/:id', (req, res) => {
const post = db.posts.find(req.params.id);
res.send(`
<h1>${escapeHtml(post.title)}</h1>
<p>${escapeHtml(post.content)}</p>
`);
});
// Using template engines (auto-escaping)
// Handlebars, EJS, etc. escape by default
res.render('post', { post }); // Safe
DOM XSS Prevention
DOM-based XSS occurs when JavaScript code constructs HTML from user input. Always use safe DOM methods or sanitization libraries.
// ❌ Bad - Using innerHTML with user data
document.getElementById('content').innerHTML = userInput;
// ✅ Good - Use textContent for text
document.getElementById('content').textContent = userInput;
// Or use safe DOM methods
const element = document.createElement('p');
element.textContent = userInput;
document.body.appendChild(element);
// Use security libraries
const DOMPurify = require('dompurify');
const clean = DOMPurify.sanitize(userInput);
document.getElementById('content').innerHTML = clean;
Cross-Site Request Forgery (CSRF)
Attacker tricks user into making unwanted requests on their behalf. CSRF tokens and SameSite cookies prevent this attack.
CSRF Protection
// Generate CSRF token
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('send', { csrfToken: req.csrfToken() });
});
// Validate CSRF token
app.post('/transfer', csrfProtection, (req, res) => {
// CSRF token automatically validated
res.json({ success: true });
});
// In HTML form
<form method="POST" action="/transfer">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<input type="text" name="amount">
<button type="submit">Transfer</button>
</form>
// For AJAX requests
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': token,
'Content-Type': 'application/json'
},
body: JSON.stringify({ amount: 100 })
});
Authentication & Authorization
Strong authentication and proper authorization checks are foundational to application security.
Secure Password Storage
const argon2 = require('argon2');
// Hash password with Argon2 (more secure than bcrypt)
async function hashPassword(password) {
return await argon2.hash(password);
}
async function verifyPassword(password, hash) {
return await argon2.verify(hash, password);
}
// Password requirements
const PASSWORD_REQUIREMENTS = {
minLength: 12,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true
};
function validatePassword(password) {
const hasUppercase = /[A-Z]/.test(password);
const hasLowercase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasSpecialChars = /[!@#$%^&*]/.test(password);
const isLongEnough = password.length >= PASSWORD_REQUIREMENTS.minLength;
return hasUppercase && hasLowercase && hasNumbers && hasSpecialChars && isLongEnough;
}
JWT Token Security
const jwt = require('jsonwebtoken');
// Generate token with expiration
function generateToken(userId) {
return jwt.sign(
{ userId },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
}
// Verify token
function verifyToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
return null;
}
}
// Store token in httpOnly cookie (not localStorage)
res.cookie('token', token, {
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'Strict', // CSRF protection
maxAge: 3600000 // 1 hour
});
Role-Based Access Control (RBAC)
const PERMISSIONS = {
ADMIN: ['create', 'read', 'update', 'delete'],
MODERATOR: ['read', 'update', 'delete'],
USER: ['read']
};
function canPerform(userRole, action) {
return PERMISSIONS[userRole]?.includes(action);
}
function authorizeAction(action) {
return (req, res, next) => {
if (!canPerform(req.user.role, action)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
app.post('/posts', authorizeAction('create'), createPost);
app.delete('/posts/:id', authorizeAction('delete'), deletePost);
Encryption & Hashing
Symmetric Encryption
const crypto = require('crypto');
function encryptData(plaintext, key) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
iv: iv.toString('hex'),
encrypted,
authTag: authTag.toString('hex')
};
}
function decryptData(encrypted, key) {
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
key,
Buffer.from(encrypted.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(encrypted.authTag, 'hex'));
let decrypted = decipher.update(encrypted.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
Secure Communication
HTTPS Only
const https = require('https');
const fs = require('fs');
const express = require('express');
const app = express();
// Redirect HTTP to HTTPS
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
res.redirect(`https://${req.header('host')}${req.url}`);
} else {
next();
}
});
const options = {
key: fs.readFileSync('path/to/key.pem'),
cert: fs.readFileSync('path/to/cert.pem')
};
https.createServer(options, app).listen(443);
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
message: 'Too many requests, please try again later.'
});
app.use('/api/', limiter);
// Stricter limit for authentication endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true
});
app.post('/login', authLimiter, loginHandler);
CORS Security
const cors = require('cors');
// ❌ Bad - Allow all origins
app.use(cors());
// ✅ Good - Whitelist specific origins
const allowedOrigins = [
'https://example.com',
'https://app.example.com'
];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));
Security Headers
const helmet = require('helmet');
app.use(helmet());
// Helmet sets these headers:
// Content-Security-Policy
// X-Frame-Options: DENY
// X-Content-Type-Options: nosniff
// X-XSS-Protection: 1; mode=block
// Strict-Transport-Security
// Referrer-Policy
Dependency Management
Check for Vulnerabilities
# NPM audit
npm audit
# Fix vulnerabilities
npm audit fix
# Automated dependency updates
npm update
# Check specific package
npm info package-name
Keep Dependencies Updated
// package.json
{
"dependencies": {
"express": "^4.18.0", // Minor version updates
"lodash": "~4.17.0" // Patch updates only
}
}
Best Practices
- Use HTTPS Everywhere: Never use HTTP in production
- Keep Dependencies Updated: Regularly update packages
- Principle of Least Privilege: Grant minimum necessary permissions
- Defense in Depth: Multiple layers of security
- Input Validation: Validate all user input
- Output Encoding: Escape all user-controlled output
- Security Testing: Include security in your test suite
- Logging & Monitoring: Track and alert on suspicious activity
- Secure Configuration: Keep secrets in environment variables
- Education: Developers must understand security
Conclusion
Web security is an ongoing process, not a one-time fix. By understanding OWASP Top 10 vulnerabilities, implementing proper authentication and authorization, using encryption correctly, and following security best practices, you can build applications that protect user data and business interests. Remember: security is everyone’s responsibility—every line of code you write either helps or hinders overall security posture.