Module A-16·17 min read

ReDoS mitigation, memory exhaustion payload attacks, event loop saturation runbooks, and cascade failure recovery for financial infrastructure.

Written by Jatin Jain Saraf · Architect of SupraScan

Module 15 — Resiliency Runbooks & High-Load Security Defenses

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: Regular Expression Denial of Service

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.

The Vulnerable Pattern

// 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

Identifying Vulnerable Patterns

Vulnerable patterns share common characteristics:

  1. Nested quantifiers: (a+)+, (a|aa)+
  2. Alternation with common prefixes: (abc|abcd)+
  3. Overlapping quantifiers: (\w+\s*)+
// 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

Safe Validation Patterns

// 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

Runtime Protection: statement_timeout for Regex

For regexes you cannot replace, enforce time limits:

// Wrap regex execution in a timeout
function safeMatch(pattern, input, timeoutMs = 10) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      reject(new Error(`Regex timeout after ${timeoutMs}ms`));
    }, timeoutMs);
 
    try {
      const result = pattern.test(input);
      clearTimeout(timer);
      resolve(result);
    } catch (err) {
      clearTimeout(timer);
      reject(err);
    }
  });
}
 
// Usage: if validation takes > 10ms, it's a ReDoS attack
try {
  const isValid = await safeMatch(complexEmailRegex, userInput, 10);
} catch (err) {
  logger.warn({ input: userInput.slice(0, 100) }, 'Potential ReDoS detected');
  return res.status(400).json({ error: 'Invalid input' });
}

JSON Payload Bombs: Memory Exhaustion via Deserialization

A crafted JSON payload can expand exponentially in memory after parsing.

// Deeply nested object: 10KB JSON → 50MB in memory
// Each nesting level multiplies reference overhead
const bomb = '{"a":{"a":{"a":{"a":{"a":{"a":...{"a":true}...}}}}}}}';
// At 500 levels deep: V8 object overhead × 500 = significant memory
 
// 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

Defense: Request Size Limits and Depth Limits

// Fastify: set request body size limit before accepting any data
const fastify = Fastify({
  bodyLimit: 1 * 1024 * 1024,  // 1MB max body size
});
 
// For high-security endpoints: even tighter limits
fastify.post('/api/v2/payments', {
  config: { bodyLimit: 10 * 1024 },  // 10KB max for payment endpoint
}, paymentHandler);
 
// Validate JSON depth before processing
function validateDepth(obj, maxDepth = 10, currentDepth = 0) {
  if (currentDepth > maxDepth) throw new Error('JSON depth limit exceeded');
  if (obj !== null && typeof obj === 'object') {
    for (const value of Object.values(obj)) {
      validateDepth(value, maxDepth, currentDepth + 1);
    }
  }
}
 
fastify.addHook('preHandler', async (request) => {
  if (request.body && typeof request.body === 'object') {
    validateDepth(request.body, 10);
  }
});

Schema-First Validation: Reject Before Parsing

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.