Complete Node.js Security Tutorial

A concise guide to securing your Node.js applications with clear explanations and minimal but practical examples.

Authentication Validation Hardening

Table of Contents

  1. Why Security Matters
  2. Authentication & Password Security
  3. Input Validation
  4. Injection Prevention
  5. XSS Prevention
  6. Security Headers
  7. Rate Limiting
  8. Environment Variables
  9. Best Practices Checklist
  10. Quick Reference
  11. Summary
  12. Interview Q&A + MCQ
  13. Contextual Learning Links

1. Why Security Matters

The Theory

Security is not optional - it is a fundamental requirement. A single vulnerability can expose user data, compromise your server, and destroy trust.

Common Security Threats

ThreatImpactReal Example
InjectionData theft/loss1 OR 1=1 returns all users
XSSSession hijacking<script>stealCookies()</script>
Weak PasswordsAccount takeoverBrute force attack
No Rate LimitingDDoS/brute forceMillions of requests

Simple Example: The Danger of Trusting Input

// ❌ NEVER DO THIS - VULNERABLE CODE
const http = require('http');
const server = http.createServer((req, res) => {
    const name = req.url.split('?name=')[1];
    res.end(`<h1>Hello ${name}</h1>`);
    // If name = "<script>alert('hacked')</script>", script executes!
});
server.listen(3000);

2. Authentication & Password Security

The Theory

Authentication verifies who you are. Authorization checks what you can do.

Password Security Golden Rules

RuleWhyBadGood
Never store plain passwordsDatabase breach exposes allpassword: "secret123"hash + salt
Use strong hashingResists brute forceMD5bcrypt/scrypt/pbkdf2
Add saltPrevents rainbow attacksHash onlyHash + unique salt
Min 8 charactersHarder to guess123456MySecur3P@ss!

Simple Example: Secure Password Storage

const crypto = require('crypto');
function hashPassword(password) {
    const salt = crypto.randomBytes(16).toString('hex');
    const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');
    return { hash, salt };
}
function verifyPassword(password, storedHash, storedSalt) {
    const hash = crypto.pbkdf2Sync(password, storedSalt, 10000, 64, 'sha512').toString('hex');
    return hash === storedHash;
}

Simple Example: Token-Based Authentication

const crypto = require('crypto');
function generateToken(userId) {
    const payload = `${userId}:${Date.now()}:${crypto.randomBytes(16).toString('hex')}`;
    const signature = crypto.createHmac('sha256', 'secret-key').update(payload).digest('hex');
    return Buffer.from(`${payload}:${signature}`).toString('base64');
}
function verifyToken(token) {
    try {
        const decoded = Buffer.from(token, 'base64').toString();
        const [userId, timestamp, nonce, signature] = decoded.split(':');
        const expected = crypto.createHmac('sha256', 'secret-key').update(`${userId}:${timestamp}:${nonce}`).digest('hex');
        if (signature !== expected) return null;
        if (Date.now() - parseInt(timestamp, 10) > 86400000) return null;
        return { userId: parseInt(userId, 10) };
    } catch { return null; }
}

3. Input Validation

The Theory

Never trust user input - validate everything before using it. Always assume input is malicious.

Validation Rules

RuleExampleWhy
Type checkingtypeof value === 'number'Prevent type confusion
Length limitsvalue.length < 100Prevent overflow
Format validationEmail regexEnsure valid format
Range checkingage >= 0 && age <= 120Prevent invalid values
Whitelist['admin','user'].includes(role)Allow known values

Simple Example: Complete Input Validation

const validators = {
    email: (email) => /^[^\s@]+@([^\s@]+\.)+[^\s@]+$/.test(email),
    username: (name) => /^[a-zA-Z0-9_]{3,20}$/.test(name),
    age: (age) => {
        const num = Number(age);
        return !isNaN(num) && Number.isInteger(num) && num >= 0 && num <= 120;
    }
};
function sanitize(input) {
    if (typeof input !== 'string') return input;
    return input.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
}

4. Injection Prevention

The Theory

Injection happens when user input is concatenated into queries and changes query logic.

// ❌ VULNERABLE
const userId = "1 OR 1=1";
db.query(`SELECT * FROM users WHERE id = ${userId}`);

Prevention Methods

MethodExample
Parameterized queriesdb.query("SELECT * FROM users WHERE id = ?", [id])
Input validationif (!/^\d+$/.test(id)) return error
Escape inputmysql.escape(userInput)

Simple Example: Parameterized Queries (Simulated)

class Database {
    constructor() { this.users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]; }
    query(sql, params) {
        if (sql.includes('id = ?')) return this.users.find(u => u.id === params[0]);
        return null;
    }
}
function isValidId(id) {
    const num = Number(id);
    return !isNaN(num) && Number.isInteger(num) && num > 0;
}

5. XSS Prevention

The Theory

XSS occurs when attackers inject scripts into pages viewed by other users.

// ❌ VULNERABLE
res.send(`<div>${comment}</div>`);
// ✅ SECURE
const escapeHtml = (text) => text.replace(//g, '>');
res.send(`<div>${escapeHtml(comment)}</div>`);

Prevention Methods

MethodHow it works
HTML Escape< to &lt;, > to &gt;
CSP HeaderBlocks unsafe inline scripts
HttpOnly CookieBlocks JS from reading cookies
Input SanitizationRemoves risky tags/handlers

Simple Example: XSS Sanitizer

function escapeHtml(text) {
    if (!text) return '';
    return text.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
}
function sanitizeHtml(text) {
    if (!text) return '';
    let cleaned = text.replace(/)<[^<]*)*<\/script>/gi, '');
    cleaned = cleaned.replace(/\son\w+\s*=\s*["'][^"']*["']/gi, '');
    return escapeHtml(cleaned);
}

6. Security Headers

The Theory

Security headers instruct browsers to behave safely and reduce common web attacks.

HeaderWhat it doesValue
X-Content-Type-OptionsPrevents MIME sniffingnosniff
X-Frame-OptionsPrevents clickjackingDENY
X-XSS-ProtectionEnables XSS filtering1; mode=block
Strict-Transport-SecurityForces HTTPSmax-age=31536000
Content-Security-PolicyRestricts resource loadingdefault-src 'self'

Simple Example: Setting Headers

function setSecurityHeaders(res) {
    res.setHeader('X-Content-Type-Options', 'nosniff');
    res.setHeader('X-Frame-Options', 'DENY');
    res.setHeader('X-XSS-Protection', '1; mode=block');
    res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
    res.setHeader('Content-Security-Policy', "default-src 'self'");
    res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
}

7. Rate Limiting

The Theory

Rate limiting restricts requests in a time window to reduce abuse, brute force, and DDoS pressure.

AttackWithout Rate LimitWith Rate Limit
Brute forceMillions of attemptsLocked after threshold
DDoSServer overwhelmedRequests throttled
API abuseUnlimited callsFair usage enforced

Simple Example: Rate Limiter Middleware

function rateLimit(maxRequests = 10, windowMs = 60000) {
    const requests = new Map();
    return (req, res, next) => {
        const ip = req.socket.remoteAddress;
        const now = Date.now();
        const timestamps = requests.get(ip) || [];
        const validRequests = timestamps.filter(t => now - t < windowMs);
        if (validRequests.length >= maxRequests) {
            res.writeHead(429, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ error: 'Too many requests' }));
            return;
        }
        validRequests.push(now);
        requests.set(ip, validRequests);
        next();
    };
}

8. Environment Variables

The Theory

Never hardcode secrets in code. Use environment variables for passwords, API keys, JWT secrets, and environment-specific config.

Why Environment Variables?

ProblemSolution
Secrets in code leak to GitStore in `.env` and secret manager
Different dev/prod configLoad from env per environment
Accidental exposureNever commit `.env`

Simple Example: Environment Checks

const PORT = process.env.PORT || 3000;
const DB_PASSWORD = process.env.DB_PASSWORD;
const JWT_SECRET = process.env.JWT_SECRET;
if (!DB_PASSWORD) { console.error('FATAL: DB_PASSWORD not set'); process.exit(1); }
if (!JWT_SECRET) { console.error('FATAL: JWT_SECRET not set'); process.exit(1); }

.env File Example

# .env file - NEVER commit this
PORT=3000
NODE_ENV=production
DB_HOST=localhost
DB_PORT=5432
DB_USER=admin
DB_PASSWORD=super-secret-password-123
JWT_SECRET=your-very-long-secret-key-here
STRIPE_SECRET_KEY=sk_live_abc123

9. Best Practices Checklist

The Theory

Security is an ongoing process, not a one-time task. Use this checklist in every release cycle.

CategoryCheckWhy
AuthenticationUse bcrypt/scrypt/pbkdf2Resist brute force
InputValidate + sanitize all inputPrevent injection/XSS
HeadersSet security headersEnable browser protections
TransportUse HTTPS + HSTSProtect data in transit
Rate limitThrottle abusive requestsPrevent abuse
SecretsUse env vars, rotate keysLimit breach damage
DependenciesRun npm audit + updatePatch known CVEs
Error handlingHide stack tracesAvoid information leakage

Simple Example: Complete Secure Server Template

const http = require('http');
const crypto = require('crypto');
const PORT = process.env.PORT || 3000;
const JWT_SECRET = process.env.JWT_SECRET || crypto.randomBytes(32).toString('hex');

function setSecurityHeaders(res) {
    res.setHeader('X-Content-Type-Options', 'nosniff');
    res.setHeader('X-Frame-Options', 'DENY');
    res.setHeader('X-XSS-Protection', '1; mode=block');
    res.setHeader('Strict-Transport-Security', 'max-age=31536000');
    res.setHeader('Content-Security-Policy', "default-src 'self'");
}

const server = http.createServer((req, res) => {
    setSecurityHeaders(res);
    res.setHeader('Content-Type', 'application/json');
    if (req.url === '/') res.end(JSON.stringify({ message: 'Secure API', status: 'healthy' }));
    else { res.statusCode = 404; res.end(JSON.stringify({ error: 'Not found' })); }
});
server.listen(PORT, () => console.log(`Secure server on ${PORT}`));

Quick Reference

Security Headers (Copy-Paste)

res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security', 'max-age=31536000');

Input Validation (Copy-Paste)

const isValidEmail = /^[^\s@]+@([^\s@]+\.)+[^\s@]+$/.test(email);
const isValidUsername = /^[a-zA-Z0-9_]{3,20}$/.test(username);
const escapeHtml = (text) => text.replace(/[&<>]/g, (m) => m === '&' ? '&' : m === '<' ? '<' : '>');

Password Hashing (Copy-Paste)

const crypto = require('crypto');
const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');

Environment Variables Check

if (!process.env.SECRET_KEY) {
    console.error('SECRET_KEY environment variable not set');
    process.exit(1);
}

Summary

Security is a mindset, not a feature.

Golden Rules

  • Never trust user input - validate everything
  • Never store plain passwords - hash with salt
  • Never hardcode secrets - use environment variables
  • Always escape output - prevent XSS
  • Always use parameterized queries - prevent injection
  • Always set security headers - enable browser protections
  • Always rate limit - prevent abuse
  • Always keep dependencies updated - patch vulnerabilities

Remember

DoDon't
Validate inputTrust user input
Hash passwordsStore plain passwords
Use env variablesHardcode secrets
Escape HTMLConcatenate user input into HTML
Use HTTPSAllow HTTP in production
Rate limitAllow unlimited requests

10 Interview Questions + 10 MCQs

Interview Pattern 10 Q&A
1Why is Node.js security critical?easy
Answer: A single vulnerability can expose data and compromise system trust.
2Why never store plain passwords?easy
Answer: Database leaks expose real user passwords immediately.
3What does salting protect against?medium
Answer: Rainbow table and identical-hash attacks.
4How do parameterized queries help?medium
Answer: They separate SQL logic from user-provided values.
5How do you prevent XSS?medium
Answer: Escape/sanitize output and set CSP.
6What does rate limiting stop?easy
Answer: Brute force attempts and abusive request floods.
7Why use environment variables?easy
Answer: To keep secrets out of code and git history.
8What does X-Frame-Options protect against?hard
Answer: Clickjacking via iframe embedding attacks.
9Why hide stack traces in production?hard
Answer: They leak internals that help attackers.
10How often should dependencies be reviewed?hard
Answer: Continuously, with regular `npm audit` and updates.

10 Node.js Security MCQs

1

Best way to store passwords?

APlain text
BHashed + salted
CBase64 only
DEncrypted reversible
Explanation: Use slow hashing with salt.
2

XSS is mainly prevented by:

AIgnoring input
BEscaping output
COnly rate limiting
DUsing GET only
Explanation: Escape/sanitize rendered content.
3

Which header blocks clickjacking?

AX-Frame-Options
BETag
CAccept
DOrigin
Explanation: Use DENY or SAMEORIGIN.
4

SQL injection is best prevented by:

AString concatenation
BParameterized queries
CConsole logging
DGET requests only
Explanation: Prepared statements isolate user data.
5

Why rate limit login endpoints?

AImprove CSS speed
BReduce brute force attacks
CDisable TLS
DIncrease payload size
Explanation: It limits repeated credential attempts.
6

Where should API keys be stored?

AIn source code
BEnvironment variables
CInside HTML comments
DPublic repo wiki
Explanation: Keep secrets out of code and git.
7

`X-Content-Type-Options: nosniff` prevents:

AMIME sniffing
BSQL injection
CSession timeout
DDNS rebinding
Explanation: It forces declared content types.
8

Most appropriate status for invalid token:

A200
B401
C304
D201
Explanation: Unauthorized access uses 401.
9

Which practice reduces exploit window in dependencies?

APin outdated versions forever
BRegular audits and updates
CDisable npm
DUse no lockfile
Explanation: Update cycles patch known CVEs.
10

Security is best viewed as:

AOne-time setup
BContinuous process
COnly frontend concern
DOnly DevOps concern
Explanation: Secure systems require ongoing review and hardening.