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