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
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
| Threat | Impact | Real Example |
|---|---|---|
| Injection | Data theft/loss | 1 OR 1=1 returns all users |
| XSS | Session hijacking | <script>stealCookies()</script> |
| Weak Passwords | Account takeover | Brute force attack |
| No Rate Limiting | DDoS/brute force | Millions 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
| Rule | Why | Bad | Good |
|---|---|---|---|
| Never store plain passwords | Database breach exposes all | password: "secret123" | hash + salt |
| Use strong hashing | Resists brute force | MD5 | bcrypt/scrypt/pbkdf2 |
| Add salt | Prevents rainbow attacks | Hash only | Hash + unique salt |
| Min 8 characters | Harder to guess | 123456 | MySecur3P@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
| Rule | Example | Why |
|---|---|---|
| Type checking | typeof value === 'number' | Prevent type confusion |
| Length limits | value.length < 100 | Prevent overflow |
| Format validation | Email regex | Ensure valid format |
| Range checking | age >= 0 && age <= 120 | Prevent 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
| Method | Example |
|---|---|
| Parameterized queries | db.query("SELECT * FROM users WHERE id = ?", [id]) |
| Input validation | if (!/^\d+$/.test(id)) return error |
| Escape input | mysql.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
| Method | How it works |
|---|---|
| HTML Escape | < to <, > to > |
| CSP Header | Blocks unsafe inline scripts |
| HttpOnly Cookie | Blocks JS from reading cookies |
| Input Sanitization | Removes 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(/