From 77e32da0bf0d43fc8d7f71f8472a97da04430214 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Tue, 13 Jan 2026 19:06:47 +0200 Subject: [PATCH 1/2] fix: improve stack frame sanitization to prevent ReDoS vulnerabilities --- libs/enclave-vm/src/adapters/vm-adapter.ts | 3 ++- libs/enclave-vm/src/double-vm/double-vm-wrapper.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/enclave-vm/src/adapters/vm-adapter.ts b/libs/enclave-vm/src/adapters/vm-adapter.ts index fd1ebb1..b0351e2 100644 --- a/libs/enclave-vm/src/adapters/vm-adapter.ts +++ b/libs/enclave-vm/src/adapters/vm-adapter.ts @@ -139,7 +139,8 @@ function sanitizeStackTrace(stack: string | undefined, sanitize = true): string // Additional: Remove line and column numbers from stack frames // Format: "at functionName (file:line:column)" -> "at functionName ([REDACTED])" - sanitized = sanitized.replace(/at\s+([^\s]+)\s+\([^)]*:\d+:\d+\)/g, 'at $1 ([REDACTED])'); + // Uses [^):]* instead of [^)]* to prevent ReDoS (colons excluded since Windows paths are already redacted) + sanitized = sanitized.replace(/at\s+(\S+)\s+\([^):]*:\d+:\d+\)/g, 'at $1 ([REDACTED])'); // Format: "at file:line:column" -> "at [REDACTED]" sanitized = sanitized.replace(/at\s+[^\s]+:\d+:\d+/g, 'at [REDACTED]'); diff --git a/libs/enclave-vm/src/double-vm/double-vm-wrapper.ts b/libs/enclave-vm/src/double-vm/double-vm-wrapper.ts index c8f07cc..d4a2acd 100644 --- a/libs/enclave-vm/src/double-vm/double-vm-wrapper.ts +++ b/libs/enclave-vm/src/double-vm/double-vm-wrapper.ts @@ -43,8 +43,8 @@ function sanitizeStackTrace(stack: string | undefined, sanitize = true): string sanitized = sanitized.replace(pattern, '[REDACTED]'); } - // Remove line/column numbers - sanitized = sanitized.replace(/at\s+([^\s]+)\s+\([^)]*:\d+:\d+\)/g, 'at $1 ([REDACTED])'); + // Remove line/column numbers (uses [^):]* instead of [^)]* to prevent ReDoS) + sanitized = sanitized.replace(/at\s+(\S+)\s+\([^):]*:\d+:\d+\)/g, 'at $1 ([REDACTED])'); sanitized = sanitized.replace(/at\s+[^\s]+:\d+:\d+/g, 'at [REDACTED]'); return sanitized; From a43661b4ce09aebb5468be62892e6ed66e8756c1 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Tue, 13 Jan 2026 19:36:34 +0200 Subject: [PATCH 2/2] fix: enhance stack trace sanitization to prevent ReDoS vulnerabilities --- libs/enclave-vm/src/adapters/vm-adapter.ts | 41 +++++++++++++++---- .../src/double-vm/double-vm-wrapper.ts | 39 ++++++++++++++++-- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/libs/enclave-vm/src/adapters/vm-adapter.ts b/libs/enclave-vm/src/adapters/vm-adapter.ts index b0351e2..aa6b227 100644 --- a/libs/enclave-vm/src/adapters/vm-adapter.ts +++ b/libs/enclave-vm/src/adapters/vm-adapter.ts @@ -118,6 +118,10 @@ const SENSITIVE_STACK_PATTERNS = [ * - Strips internal hostnames and IPs * - Removes line/column numbers for full anonymization * + * Uses line-by-line processing with pre-checks to prevent ReDoS attacks. + * The vulnerable pattern /at\s+(\S+)\s+\([^)]*:\d+:\d+\)/g can cause polynomial + * backtracking on malicious input like "at ! (at ! (at ! (...". + * * @param stack Original stack trace * @param sanitize Whether to sanitize (defaults to true) * @returns Sanitized stack trace (or original if sanitize=false) @@ -137,15 +141,38 @@ function sanitizeStackTrace(stack: string | undefined, sanitize = true): string sanitized = sanitized.replace(pattern, '[REDACTED]'); } - // Additional: Remove line and column numbers from stack frames - // Format: "at functionName (file:line:column)" -> "at functionName ([REDACTED])" - // Uses [^):]* instead of [^)]* to prevent ReDoS (colons excluded since Windows paths are already redacted) - sanitized = sanitized.replace(/at\s+(\S+)\s+\([^):]*:\d+:\d+\)/g, 'at $1 ([REDACTED])'); + // Remove line/column numbers - process line by line with pre-checks to prevent ReDoS + // Pre-checking the ending pattern before applying full regex avoids polynomial backtracking + const lines = sanitized.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Defense-in-depth: skip very long lines + if (line.length > 1000) continue; + + // Pattern 1: "at functionName (path:line:column)" format + // Quick check: does line contain ":digits:digits)" pattern? + if (/:\d+:\d+\)/.test(line)) { + // Extract function name using indexOf to avoid regex backtracking + const atIdx = line.indexOf('at '); + if (atIdx !== -1) { + const afterAt = line.substring(atIdx + 3).trimStart(); + const spaceIdx = afterAt.indexOf(' '); + if (spaceIdx !== -1 && afterAt.charAt(spaceIdx + 1) === '(') { + const funcName = afterAt.substring(0, spaceIdx); + lines[i] = line.substring(0, atIdx) + 'at ' + funcName + ' ([REDACTED])'; + continue; + } + } + } - // Format: "at file:line:column" -> "at [REDACTED]" - sanitized = sanitized.replace(/at\s+[^\s]+:\d+:\d+/g, 'at [REDACTED]'); + // Pattern 2: "at path:line:column" format (no parentheses) + if (/:\d+:\d+$/.test(line) && /^\s*at\s/.test(line)) { + lines[i] = line.replace(/:\d+:\d+$/, '') + '[REDACTED]'; + } + } - return sanitized; + return lines.join('\n'); } /** diff --git a/libs/enclave-vm/src/double-vm/double-vm-wrapper.ts b/libs/enclave-vm/src/double-vm/double-vm-wrapper.ts index d4a2acd..ca2abc4 100644 --- a/libs/enclave-vm/src/double-vm/double-vm-wrapper.ts +++ b/libs/enclave-vm/src/double-vm/double-vm-wrapper.ts @@ -33,6 +33,10 @@ const SENSITIVE_STACK_PATTERNS = [ /** * Sanitize stack trace by removing host file system paths + * + * Uses line-by-line processing with pre-checks to prevent ReDoS attacks. + * The vulnerable pattern /at\s+(\S+)\s+\([^)]*:\d+:\d+\)/g can cause polynomial + * backtracking on malicious input like "at ! (at ! (at ! (...". */ function sanitizeStackTrace(stack: string | undefined, sanitize = true): string | undefined { if (!stack || !sanitize) return stack; @@ -43,11 +47,38 @@ function sanitizeStackTrace(stack: string | undefined, sanitize = true): string sanitized = sanitized.replace(pattern, '[REDACTED]'); } - // Remove line/column numbers (uses [^):]* instead of [^)]* to prevent ReDoS) - sanitized = sanitized.replace(/at\s+(\S+)\s+\([^):]*:\d+:\d+\)/g, 'at $1 ([REDACTED])'); - sanitized = sanitized.replace(/at\s+[^\s]+:\d+:\d+/g, 'at [REDACTED]'); + // Remove line/column numbers - process line by line with pre-checks to prevent ReDoS + // Pre-checking the ending pattern before applying full regex avoids polynomial backtracking + const lines = sanitized.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Defense-in-depth: skip very long lines + if (line.length > 1000) continue; + + // Pattern 1: "at functionName (path:line:column)" format + // Quick check: does line contain ":digits:digits)" pattern? + if (/:\d+:\d+\)/.test(line)) { + // Extract function name using indexOf to avoid regex backtracking + const atIdx = line.indexOf('at '); + if (atIdx !== -1) { + const afterAt = line.substring(atIdx + 3).trimStart(); + const spaceIdx = afterAt.indexOf(' '); + if (spaceIdx !== -1 && afterAt.charAt(spaceIdx + 1) === '(') { + const funcName = afterAt.substring(0, spaceIdx); + lines[i] = line.substring(0, atIdx) + 'at ' + funcName + ' ([REDACTED])'; + continue; + } + } + } + + // Pattern 2: "at path:line:column" format (no parentheses) + if (/:\d+:\d+$/.test(line) && /^\s*at\s/.test(line)) { + lines[i] = line.replace(/:\d+:\d+$/, '') + '[REDACTED]'; + } + } - return sanitized; + return lines.join('\n'); } /**