Security Deep Dive
Node.js Advanced
Chapter 4 of 8 · Security Deep Dive
Security Deep Dive
The security courses on XSS, CSRF, SQLi, and authentication cover the universal web vulnerabilities. This chapter covers the ones that bite Node specifically: prototype pollution (a JavaScript quirk that lets attackers corrupt shared objects), ReDoS (a regex backtracking trap that freezes the event loop), SSRF (your server being tricked into fetching internal services), path traversal, timing leaks, and supply chain risks via npm. Node's single-threaded model makes some of these hurt more than in multi-threaded runtimes — a frozen event loop means every request stalls.
1 — Prototype Pollution
In JavaScript, Object.prototype is the root of nearly every object's prototype chain. If an attacker can write a property onto Object.prototype, that property appears on every plain object in the process — including objects your code creates internally and checks with if (obj.admin).
Prototype Pollution Defences
🔴 Libraries Historically Vulnerable to Prototype Pollution
Lodash (pre-4.17.21), jQuery $.extend (pre-3.4.0), Hoek, minimist, and many "deep merge / deep clone" utilities have had prototype pollution CVEs. Always run npm audit and update these immediately.
The pattern to search your codebase for: any function that iterates for (key in source) and writes target[key] without checking the key — including your own code.
2 — ReDoS (Regular Expression Denial of Service)
Certain regex patterns have exponential backtracking: for a carefully crafted input string, the engine tries an astronomical number of possible matches before concluding there's no match. In Node's single-threaded model, an attacker-controlled string hitting a vulnerable regex freezes the event loop — every other request stalls until the regex engine gives up.
3 — SSRF (Server-Side Request Forgery)
SSRF happens when your Node code makes an HTTP request using a URL that an attacker controls. The attacker's goal is to make your server talk to an internal service it shouldn't — a metadata API, an internal admin interface, Redis, the database, or cloud provider metadata endpoints like 169.254.169.254.
4 — Path Traversal
5 — Timing Attacks on Secret Comparison
6 — npm Audit & Supply Chain Security
| Supply Chain Attack | How it works | Defence |
|---|---|---|
| Typosquatting | lodash vs lodahs — a malicious package with a typo'd name |
Triple-check package names; use npm audit; internal registry mirror |
| Dependency confusion | Attacker publishes a public package with the same name as your private package — npm may pull the higher-versioned public one | Scope private packages (@yourcompany/pkg); set registry in .npmrc per scope |
| Malicious postinstall | A package's postinstall script runs arbitrary shell commands on npm install |
ignore-scripts=true in .npmrc; review scripts before installing unfamiliar packages |
| Account takeover | Attacker hijacks a maintainer's npm account and publishes a malicious version | Enable 2FA on npm accounts; pin exact versions in package.json; use npm audit signatures |
💡 Node.js Permission Model (v20+)
Node 20 introduced an experimental permissions flag: node --experimental-permission --allow-fs-read=/uploads server.js
This restricts what the process can access at the OS level — the process can't read /etc/passwd even if the code tries. It also supports --allow-fs-write, --allow-net, --allow-child-process, and --allow-worker. Still experimental but directionally important for defence-in-depth.
Child Process Shell Injection
Covered in Chapter 3 (node2-3): using exec() with user-controlled strings allows command injection. Always use execFile() or spawn() with an args array, never build shell strings from user input.
Environment Variable Leakage
Never log process.env directly — it contains every secret the process was started with. If an error handler serializes the request context and accidentally includes env vars, those secrets appear in logs. Use the createSafeLogger pattern from node2-6.
Security Headers Checklist
Add helmet middleware to any Express app: app.use(require('helmet')()). Sets X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security, Content-Security-Policy defaults, and removes the X-Powered-By: Express fingerprint.
Rate Limiting
Use express-rate-limit to cap requests per IP: rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }). Apply tighter limits to auth endpoints. Combine with slow-down to add progressive delays before the hard limit.
Coding Challenges
Challenge 1 — Safe Deep Merge
Write a deepMerge(target, ...sources) function that blocks prototype pollution (rejects __proto__, constructor, prototype keys at any depth), handles arrays by replacing (not merging) them, and correctly merges nested plain objects. Write 8 tests that cover: normal merge, nested merge, array replacement, and four prototype-pollution attack variants (__proto__, constructor.prototype, via JSON.parse, and a deeply nested payload). All four attack tests must leave Object.prototype clean after the call.
Challenge 2 — ReDoS Detector
Write a testRegexSafety(pattern, inputs, timeoutMs) function that runs a list of test inputs against a regex pattern, each in a Worker Thread with the given timeout. It returns a report: { safe: boolean, results: [{ input, matched, durationMs, timedOut }] } — safe is false if any input timed out. Demonstrate it with three patterns: a safe one (/^a+$/), a ReDoS-vulnerable one (/^(a+)+$/), and an email validator. For the vulnerable pattern, use an attack input ('a'.repeat(30) + 'b') that should time out and prove it returns safe: false.
Challenge 3 — SSRF-Safe Fetch Middleware
Build an Express middleware ssrfGuard(allowedDomains) that reads a url query parameter, validates it (https only, domain in allowlist, resolves to a public IP), and attaches the validated URL to req.safeUrl — or sends a 400 with a descriptive error if it fails. Test the middleware with 6 inputs that should fail (http URL, file:// URL, a private IP URL, a localhost URL, a domain not in the allowlist, and a DNS rebinding simulation) and 1 that should pass. Use nock or manual DNS mocking to avoid real network calls in tests.