From 8d0ee790b4e9a363573e929f87ea098d48b0b4fd Mon Sep 17 00:00:00 2001 From: Rizwan Saleem Date: Sun, 2 Aug 2026 13:32:02 +0100 Subject: [PATCH] Fix quadratic parsing of leading whitespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `geckoRe` and `javaScriptCoreRe` began with `^\s*` followed immediately by a group that also matches whitespace — `(.*?)` and `([^@]*)` respectively. A leading run of spaces could therefore be divided between the two in n ways, and a line that did not match forced the engine through every split: n= 1000 4.2 ms n= 2000 14.6 ms n= 4000 57.9 ms n= 8000 231.3 ms n=16000 924.0 ms `err.stack` embeds the message, so attacker-influenced text reaches this directly: a 30k-space message took ~3.3s of blocked event loop. The leading `\s*` is redundant, since the following group already matches whitespace. Its only real effect was keeping leading whitespace out of the captured methodName, so that is handled where the capture is read. After: 8000 -> 0.16ms, 16000 -> 0.37ms, 32000 -> 0.63ms, ~2x per doubling. `chromeRe`, `winjsRe` and `nodeRe` are untouched: each has a literal `at ` after `^\s*`, which anchors the boundary and prevents the ambiguity. --- src/stack-trace-parser.js | 8 ++++---- test/stack-trace-parser.spec.js | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/stack-trace-parser.js b/src/stack-trace-parser.js index 30a8756..c64e2df 100644 --- a/src/stack-trace-parser.js +++ b/src/stack-trace-parser.js @@ -71,7 +71,7 @@ function parseWinjs(line) { }; } -const geckoRe = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|rsc|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i; +const geckoRe = /^(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|rsc|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i; const geckoEvalRe = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i; function parseGecko(line) { @@ -93,14 +93,14 @@ function parseGecko(line) { return { file: parts[3], - methodName: parts[1] || UNKNOWN_FUNCTION, + methodName: (parts[1] && parts[1].trim()) || UNKNOWN_FUNCTION, arguments: parts[2] ? parts[2].split(',') : [], lineNumber: parts[4] ? +parts[4] : null, column: parts[5] ? +parts[5] : null, }; } -const javaScriptCoreRe = /^\s*(?:([^@]*)(?:\((.*?)\))?@)?(\S.*?):(\d+)(?::(\d+))?\s*$/i; +const javaScriptCoreRe = /^(?:([^@]*)(?:\((.*?)\))?@)?(\S.*?):(\d+)(?::(\d+))?\s*$/i; function parseJSC(line) { const parts = javaScriptCoreRe.exec(line); @@ -111,7 +111,7 @@ function parseJSC(line) { return { file: parts[3], - methodName: parts[1] || UNKNOWN_FUNCTION, + methodName: (parts[1] && parts[1].trim()) || UNKNOWN_FUNCTION, arguments: [], lineNumber: +parts[4], column: parts[5] ? +parts[5] : null, diff --git a/test/stack-trace-parser.spec.js b/test/stack-trace-parser.spec.js index ff16c17..fa4e3c2 100644 --- a/test/stack-trace-parser.spec.js +++ b/test/stack-trace-parser.spec.js @@ -3,6 +3,30 @@ import * as stackTraceParser from '../src'; import CapturedExceptions from './fixtures/captured-errors'; describe('stackTraceParser', () => { + // `geckoRe` and `javaScriptCoreRe` began with `^\\s*` followed by a group that + // also matches whitespace, so a leading run of spaces could be split between + // them in n ways and parsing a non-matching line was quadratic in its length. + // A 30k-character run took over three seconds; err.stack embeds the message, + // so attacker-influenced text reaches this directly. + it('parses a long leading whitespace run in linear time', function () { + this.timeout(5000); + + const time = (n) => { + const line = ' '.repeat(n) + 'x'; + const start = Date.now(); + stackTraceParser.parse(line); + return Date.now() - start; + }; + + time(4000); // warm up + const small = time(8000); + const large = time(32000); + + // Quadratic would be ~16x for a 4x input; allow generous headroom for a + // slow or noisy CI machine while still failing on the old behaviour. + expect(large).to.be.lessThan(Math.max(small, 1) * 8 + 100); + }); + it('parses node error with space in path', () => { const stackFrames = stackTraceParser.parse( CapturedExceptions.NODE_SPACE.stack