From 1d7fb82b5a2c85e3f01b63626f433795a1d40c6a Mon Sep 17 00:00:00 2001 From: Rishaank Gupta Date: Sat, 29 Aug 2026 00:48:10 +0530 Subject: [PATCH 1/2] fix: O(1) fast-reject for unclosed link syntax --- src/Lexer.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Lexer.ts b/src/Lexer.ts index bc12e9f3be..e33fb3615c 100644 --- a/src/Lexer.ts +++ b/src/Lexer.ts @@ -385,6 +385,12 @@ export class _Lexer { let keepPrevChar = false; let prevChar = ''; let srcLength = Infinity; + // One-time precomputation (O(n)) so the link-tokenizer fast-reject + // below can be an O(1) check per loop iteration instead of re-scanning + // the shrinking remainder of src on every call (which would itself be + // O(n) per call, and O(n^2) in aggregate across the loop). + const originalLength = src.length; + const lastCloseParenIndex = src.lastIndexOf(')'); while (src) { if (src.length < srcLength) { srcLength = src.length; @@ -427,7 +433,13 @@ export class _Lexer { } // link - if (token = this.tokenizer.link(src)) { + // Fast-reject: rules.inline.link structurally requires a literal ')' + // to close the destination. If none remains ahead in src, matching + // is impossible, so skip the (potentially expensive) regex attempt. + // consumedLength + lastCloseParenIndex comparison is O(1); avoids + // algorithmic-complexity DoS via long runs of unclosed "[x](". + const consumedLength = originalLength - src.length; + if (lastCloseParenIndex >= consumedLength && (token = this.tokenizer.link(src))) { src = src.substring(token.raw.length); tokens.push(token); continue; From 31ae810721caa1f6bc2524b0eeec021f16746e4c Mon Sep 17 00:00:00 2001 From: Rishaank Gupta Date: Sat, 29 Aug 2026 00:52:04 +0530 Subject: [PATCH 2/2] Add quadratic_link_unclosed_repeated test case --- test/specs/redos/quadratic_link_unclosed_repeated.cjs | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 test/specs/redos/quadratic_link_unclosed_repeated.cjs diff --git a/test/specs/redos/quadratic_link_unclosed_repeated.cjs b/test/specs/redos/quadratic_link_unclosed_repeated.cjs new file mode 100644 index 0000000000..92b3483286 --- /dev/null +++ b/test/specs/redos/quadratic_link_unclosed_repeated.cjs @@ -0,0 +1,4 @@ +module.exports = { + markdown: '[a]('.repeat(20000) + 'b', + html: '

' + '[a]('.repeat(20000) + 'b

\n', +};