Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion lib/marked.js
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,13 @@ inline.tag = edit(inline.tag)
inline._punctuation = '!"#$%&\'()*+,\\-./:;<=>?@\\[^_{|}~';
inline.em = edit(inline.em).replace(/punctuation/g, inline._punctuation).getRegex();

inline._inside = /(?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*/;
// The bracket-pair branch uses [^\[\]]* (not [^\]]*) so a single pair cannot
// itself contain '[' . Allowing '[' there made this branch overlap the outer
// repetition, causing catastrophic backtracking on nested brackets such as
// "[[[[...x...]]]]" (a ~4KB message froze the parser for seconds). Restricting
// it removes the ambiguity while still allowing one level of [..] nesting in
// link text and preserving the stray-']' case (see nested_square_link test).
inline._inside = /(?:\[[^\[\]]*\]|\](?=[^\[]*\])|[^\[\]])*/;
inline._href = /(?:[^()]|\([^()]*\)|\((?:[^()]*\([^()]*\))+[^()]*\))*/;
inline.link = replace(inline.link)
('inside', inline._inside)
Expand Down
51 changes: 51 additions & 0 deletions test/redos.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env node

/**
* Regression test for catastrophic backtracking (ReDoS) in the inline
* link/reflink "_inside" grammar.
*
* Before the fix, a single ~4 KB message of balanced nested brackets
* ("[[[[ ... x ... ]]]]") blocked the parser for ~2.6s, scaling
* super-linearly (8 KB -> ~20s). marked runs synchronously on the
* render thread in consumers such as the Mattermost web/desktop client,
* so one such chat message froze every viewer's UI.
*
* This asserts the same payloads now render well under a strict time
* budget. Run with: `node test/redos.js`
*/

var assert = require('assert');
var marked = require('../');

// Options used by the Mattermost webapp consumer.
var options = { sanitize: true, gfm: true, tables: true, mangle: false };

var BUDGET_MS = 250;

var cases = [
['balanced nested brackets (4 KB)',
'['.repeat(2000) + 'x' + ']'.repeat(2000) + '(u)'],
['balanced nested brackets (8 KB)',
'['.repeat(4000) + 'x' + ']'.repeat(4000) + '(u)'],
['reflink form',
'['.repeat(3000) + 'x' + ']'.repeat(3000) + '][1]'],
['"a][" alternation',
'[' + 'a]['.repeat(3000) + '](u)'],
['repeated nested pairs',
'[a[b]'.repeat(2000) + '](u)']
];

var failed = 0;
cases.forEach(function(c) {
var start = Date.now();
marked(c[1], options);
var elapsed = Date.now() - start;
var ok = elapsed < BUDGET_MS;
console.log((ok ? 'ok ' : 'FAIL ') + c[0] + ' (' + c[1].length +
' chars) -> ' + elapsed + ' ms');
if (!ok) failed++;
});

assert.strictEqual(failed, 0,
failed + ' ReDoS case(s) exceeded the ' + BUDGET_MS + 'ms budget');
console.log('\nAll ReDoS regression cases rendered within ' + BUDGET_MS + 'ms.');
1 change: 1 addition & 0 deletions test/tests/mm_redos_nested_brackets.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p><a href="http://example.com">outer [inner] text</a> and [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[x]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] trailing</p>
1 change: 1 addition & 0 deletions test/tests/mm_redos_nested_brackets.text
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[outer [inner] text](http://example.com) and [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[x]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] trailing