What this module covers: A blockchain indexer processing 50,000 events/second is a valuable target. An attacker who can send one malicious payload that blocks the event loop for 30 seconds can effectively take the service offline. A ReDoS attack requires only an HTTP request. A memory exhaustion attack requires only a cleverly crafted JSON payload. This module covers the exact attack surfaces in high-throughput Node.js applications, the defensive patterns that prevent them, and the runbooks your team needs written down before the incident happens — because the worst time to write a runbook is during an outage.
ReDoS exploits catastrophic backtracking in certain regular expression patterns. When these patterns are given carefully crafted input, the regex engine's backtracking algorithm takes exponential time.
// This looks harmless: const emailValidator = /^([a-zA-Z0-9])(([a-zA-Z0-9])|(\.))+@[a-zA-Z0-9]+\.[a-zA-Z]{2,4}$/;
// Input that causes catastrophic backtracking: const maliciousInput = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaa@'; // The regex engine tries all possible ways to match the repeating groups // before concluding there's no match — exponential time
// VULNERABLE patterns (never use in hot paths with untrusted input): /^(a+)+$/ // nested quantifier /^([a-z]+)*$/ // nested quantifier with alternation /^(.*)(foo)(.*)(bar)(.*)$/ // polynomial backtracking on large strings /(\w+\s)+\w+/ // matching whitespace-separated words
// V8 (and therefore Node.js) does NOT support atomic groups or possessive quantifiers. // `(?:pattern)` is a plain non-capturing group — it does nothing to prevent backtracking. // `(?>pattern)` is the real atomic-group syntax, but it's an unshipped TC39 proposal; // using it in standard JavaScript throws a SyntaxError. Don't reach for either as a fix.
// SAFE approach 1: restructure the pattern to remove the nested quantifier // (the vulnerable email regex from above rewritten with a single bounded quantifier // and no ambiguity between the two alternatives that can both match the same input) const emailValidatorSafe = /^[a-zA-Z0-9](?:[a-zA-Z0-9.]){0,63}@[a-zA-Z0-9]+\.[a-zA-Z]{2,4}$/;
// SAFE approach 2: enforce a length cap before the regex ever runs — most catastrophic // backtracking is only exploitable because the input is unbounded function validateBounded(pattern, input, maxLength = 254) { if (input.length > maxLength) return false; return pattern.test(input); }
// SAFE approach 3: for patterns you can't safely rewrite, run them through the `re2` // npm package instead of the native regex engine. RE2 uses a linear-time automaton // and cannot exhibit catastrophic backtracking, by construction: import RE2 from 're2'; const reDosProofPattern = new RE2(/^([a-zA-Z0-9])(([a-zA-Z0-9])|(\.))+@[a-zA-Z0-9]+\.[a-zA-Z]{2,4}$/); console.log(reDosProofPattern.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaa@')); // returns false in linear time, no hang
// For payment systems: validate wallet addresses with bounded patterns const EVM_ADDRESS = /^0x[0-9a-fA-F]{40}$/; // exact length, no backtracking const TX_HASH = /^0x[0-9a-fA-F]{64}$/; // exact length, no backtracking const AMOUNT = /^\d{1,20}$/; // bounded digits, linear
// For UPI payment IDs: const UPI_ID = /^[\w.\-]{1,64}@[\w.\-]{1,64}$/; // bounded, no nested quantifiers
// Test all validators against adversarial input before deploying import { safe as safeRegex } from 'safe-regex'; console.log(safeRegex(/^(a+)+$/)); // false — unsafe! console.log(safeRegex(EVM_ADDRESS)); // true — safe
// Repeated keys object: forces V8 to store all duplicates const repeated = '{"key": "val", "key": "val", "key": "val", ...}'; // 100K repetitions // JSON.parse keeps only last value, but parses ALL entries
// Large arrays of objects: straightforward amplification const explosion = JSON.stringify({ data: Array(1_000_000).fill({ id: 1, value: 'x' }) }); // 10KB JSON → 800MB JavaScript array in memory
The most effective defense: use ajv's compiled schema to validate structure before your application code runs.
// Fastify schema validation runs BEFORE your handler // Invalid payloads are rejected by the compiled ajv function fastify.post('/api/v2/payments', { schema: { body: { type: 'object', maxProperties: 10, // max 10 keys at root additionalProperties: false, // no unknown keys required: ['amount', 'senderId', 'recipientId'], properties: { amount: { type: 'integer', minimum: 1, maximum: 1_000_000_000 }, senderId: { type: 'string', maxLength: 64 }, recipientId: { type: 'string', maxLength: 64 }, } } } }, paymentHandler); // Payloads with extra keys, wrong types, or out-of-range values // are rejected with 400 before the handler runs // No JSON bomb can make it past the schema validator
Any synchronous operation on the event loop is an attack surface: if an attacker can cause your code to execute a long synchronous operation, the entire service is blocked.
Even with size limits, a 1MB JSON payload takes ~8ms to parse synchronously. At 50K req/sec, if even 1% of requests are 1MB payloads: 500 req/sec × 8ms = 4,000ms/sec of blocking — event loop ELU 400% (impossible — starvation).
// Protection: move large payload parsing to worker threads const LARGE_PAYLOAD_THRESHOLD = 50 * 1024; // 50KB
// SAFE: use async crypto APIs const key = await new Promise((resolve, reject) => { crypto.scrypt(password, salt, 64, (err, key) => { if (err) reject(err); else resolve(key); }); }); // This runs in the libuv thread pool, not on the event loop
When an upstream service (database, external API, blockchain RPC node) becomes slow or unavailable, requests back up — each waiting for a timeout. This cascades: slow upstream → slow application → slow everything else → OOM.
A circuit breaker short-circuits failing calls immediately after a failure threshold, giving the upstream time to recover.
If your service makes calls to multiple upstream services, a slow upstream should not exhaust the connection pool for all upstreams.
// Separate connection pools per upstream (bulkhead isolation) const pools = { postgresql: new pg.Pool({ max: 30 }), // 30 connections for DB analyticsDb: new pg.Pool({ max: 10 }), // 10 separate connections for analytics externalRpc: new Agent({ maxSockets: 20 }), // 20 connections to blockchain RPC };
// If analyticsDb is slow, it can exhaust its 10 connections // but never affects the 30 PostgreSQL connections // The main write path is isolated from analytics slowness
Symptoms: ELU > 0.90, high latency, db_pool_waiting_count > 0 but CPU looks idle.
# Step 1: Confirm ELU via metrics # ELU metric: nodejs_event_loop_utilization > 0.90
# Step 2: Generate CPU profile to find the blocking function clinic flame -- node indexer.js & SERVER_PID=$! autocannon -c 50 -d 20 http://localhost:3000/health kill $SERVER_PID # Opens flamegraph: look for wide plateau in non-I/O functions
# Step 3: If production emergency, reduce load # Scale up replicas immediately (buy time) kubectl scale deployment indexer --replicas=16
# Step 4: Identify and fix # Common causes: JSON.parse in hot path, sync crypto, RegEx on large input # Fix: move to worker thread or replace with async equivalent
Symptoms: Heap memory growing continuously over hours, GC running but memory not dropping.
# Step 1: Confirm via heap metric # nodejs_heap_used_bytes growing without leveling off
# Step 2: Take two heap snapshots kill -SIGUSR2 $PID # snapshot 1 (configured in app startup) sleep 300 kill -SIGUSR2 $PID # snapshot 2
# Step 3: Load snapshots in Chrome DevTools # Memory tab → Load heap snapshot → Switch to Comparison view # Sort by Delta (objects that increased between snapshots) # Common leak sources: EventEmitter listeners, Map/Set without eviction, closures
# Step 4: Emergency mitigation while fix is deployed # PM2: set max_memory_restart to trigger automatic restart before OOM # pm2 set max_memory_restart 2G
Symptoms:db_pool_waiting_count > 0, P99 latency spike to connectionTimeoutMillis.
# Step 1: Confirm via metrics # db_pool_waiting_count > 0
# Step 2: Check what's holding connections # In PostgreSQL: psql -c "SELECT pid, state, query_start, left(query,100) FROM pg_stat_activity WHERE state IN ('active','idle in transaction') ORDER BY query_start;"
# Step 3: Kill long-running queries holding connections psql -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle in transaction' AND query_start < NOW() - INTERVAL '30s';"
# Step 4: Temporary relief # Increase pool size if database can handle more connections # Update and reload (zero-downtime with PM2 reload): # pool.options.max = 60; (requires restart to take effect)
# Step 5: Find root cause # Was there a spike in traffic? → Size pool for peak # Did a query get slow? → Check EXPLAIN ANALYZE for plan regression
# Step 1: Confirm and measure kafka-consumer-groups.sh --bootstrap-server kafka:9092 \ --group transaction-processor --describe # Shows lag per partition
# Step 2: Identify the slow partition (hotspot?) # If one partition has 80% of total lag: partition key imbalance # If all partitions have equal lag: consumer is generally too slow
# Step 3: Scale consumers kubectl scale deployment analytics-service --replicas=12 # Kafka will rebalance partitions to new consumers
# Step 4: Check consumer processing time # If avg processing > 10ms per message at 50K msg/sec: can't keep up # Fix: batch processing, optimize DB writes (use bulk INSERT)
# Step 5: If lag is from historical event replay (deployment with fromBeginning): # Reset consumer offset to current kafka-consumer-groups.sh --reset-offsets --to-latest \ --group transaction-processor --topic transactions --execute
Symptoms: One upstream service goes down, your service starts failing, downstream services start failing.
# Step 1: Identify the failing upstream # Check circuit breaker state metrics: circuit_state{service="blockchain-rpc"} = OPEN
# Step 2: Verify circuit breaker is isolating the failure # If circuit is OPEN: failing fast → good, cascade is contained # If circuit is CLOSED but service is slow: trigger manual circuit open
# In application code: blockchainRpcBreaker.forceOpen(); # expose this via admin endpoint
# Step 3: Enable degraded mode # Return cached/stale data while upstream recovers # Defer non-critical operations (analytics, notifications) # Prioritize critical path (transaction validation, DB writes)
# Step 4: Monitor upstream recovery # Watch circuit breaker half-open probes # circuit_state will transition: OPEN → HALF_OPEN → CLOSED when upstream recovers
# Step 5: Gradually restore traffic # Circuit breaker handles this automatically via HALF_OPEN state # Verify: watch success rate of operations through the recovered circuit
# Lock exact versions in production # Never use ^ or ~ in production package.json { "dependencies": { "fastify": "4.28.1", # exact, not ^4.28.1 "pg": "8.12.0" # exact } }
// Restrict what packages can do at runtime using Node.js Permission Model. // --permission is the stable flag (stabilized in v23.5.0; older Node versions used // --experimental-permission). Without it, --allow-fs-read etc. are no-ops. // node --permission --allow-fs-read=/app/config indexer.js // Any attempt to read files outside /app/config → throws an ERR_ACCESS_DENIED error
// Note: the Permission Model governs fs access, child_process, worker_threads, and // native addons — it does NOT govern outbound network connections. There is no // --allow-net flag in Node.js (that's a Deno feature). Restricting which hosts a // compromised dependency can reach requires network-layer controls instead — e.g. // egress firewall rules, a service mesh policy, or a Kubernetes NetworkPolicy.
// This protects against compromised dependencies that try to: // - Exfiltrate environment variables (fs read outside allowed paths is blocked) // - Write files to disk (ransomware) — fs write outside allowed paths is blocked // - Spawn child processes or worker threads — blocked unless explicitly allowed // Network exfiltration must be stopped at the infrastructure layer, not by this flag.
Exact versions in production. npm audit. Node.js Permission Model to restrict runtime capabilities.
The system is secure and resilient. The remaining modules cover the advanced Node.js features that eliminate entire categories of deployment, security, and performance problems: zero-trust runtime isolation, single executable deployment, native Rust integration, the Web Standards shift, and automated post-mortem diagnostics.
Next: Module 16 — Zero-Trust Runtime Architecture & The Node.js Permission Model →
Knowledge Check
A Node.js application receives a massive JSON payload with a deeply nested structure (e.g., hundreds of levels of nesting). Which of the following is the most robust, schema-first defense mechanism against memory exhaustion (JSON bombs) before the application logic even runs?
How does the Bulkhead pattern differ from a Circuit Breaker when designing resilient Node.js services?
During an incident, the Event Loop Utilization (ELU) spikes above 0.90, latency is high, but CPU usage appears idle. Following the runbook, what is the most appropriate next step to diagnose the root cause?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.