Skip to content

Commit

Permalink
Improve worst-case performance of inline.text regex
Browse files Browse the repository at this point in the history
The old regex may take quadratic time to scan for potential line
breaks or email addresses starting at every point.  Fix it to avoid
scanning from points that would have been in the middle of a previous
scan.

Signed-off-by: Anders Kaseorg <andersk@mit.edu>
  • Loading branch information
andersk committed Apr 4, 2019
1 parent ba1de1e commit be27472
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 8 deletions.
9 changes: 3 additions & 6 deletions lib/marked.js
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ var inline = {
code: /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,
br: /^( {2,}|\\)\n(?!\s*$)/,
del: noop,
text: /^(`+|[^`])[\s\S]*?(?=[\\<!\[`*]|\b_| {2,}\n|$)/
text: /^(`+|[^`])(?:[\s\S]*?(?:(?=[\\<!\[`*]|\b_|$)|[^ ](?= {2,}\n))|(?= {2,}\n))/
};

// list of punctuation marks from common mark spec
Expand Down Expand Up @@ -615,10 +615,7 @@ inline.gfm = merge({}, inline.normal, {
url: /^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,
_backpedal: /(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,
del: /^~+(?=\S)([\s\S]*?\S)~+/,
text: edit(inline.text)
.replace(']|', '~]|')
.replace('|$', '|https?://|ftp://|www\\.|[a-zA-Z0-9.!#$%&\'*+/=?^_`{\\|}~-]+@|$')
.getRegex()
text: /^(`+|[^`])(?:[\s\S]*?(?:(?=[\\<!\[`*~]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@))|(?= {2,}\n|[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@))/
});

inline.gfm.url = edit(inline.gfm.url, 'i')
Expand All @@ -630,7 +627,7 @@ inline.gfm.url = edit(inline.gfm.url, 'i')

inline.breaks = merge({}, inline.gfm, {
br: edit(inline.br).replace('{2,}', '*').getRegex(),
text: edit(inline.gfm.text).replace('{2,}', '*').getRegex()
text: edit(inline.gfm.text).replace(/\{2,\}/g, '*').getRegex()
});

/**
Expand Down
4 changes: 4 additions & 0 deletions test/redos/quadratic_br.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
markdown: `a${' '.repeat(50000)}`,
html: `<p>a${' '.repeat(50000)}</p>`
};
4 changes: 4 additions & 0 deletions test/redos/quadratic_email.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
markdown: 'a'.repeat(50000),
html: `<p>${'a'.repeat(50000)}</p>`
};
3 changes: 1 addition & 2 deletions test/specs/gfm/gfm.0.28.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,7 @@
"section": "Autolinks",
"html": "<p><a href=\"mailto:a.b-c_d@a.b\">a.b-c_d@a.b</a></p>\n<p><a href=\"mailto:a.b-c_d@a.b\">a.b-c_d@a.b</a>.</p>\n<p>a.b-c_d@a.b-</p>\n<p>a.b-c_d@a.b_</p>",
"markdown": "a.b-c_d@a.b\n\na.b-c_d@a.b.\n\na.b-c_d@a.b-\n\na.b-c_d@a.b_",
"example": 607,
"shouldFail": true
"example": 607
},
{
"section": "Disallowed Raw HTML",
Expand Down
24 changes: 24 additions & 0 deletions test/specs/redos-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const path = require('path');
const fs = require('fs');

const redosDir = path.resolve(__dirname, '../redos');

describe('ReDOS tests', () => {
const files = fs.readdirSync(redosDir);
files.forEach(file => {
if (!file.match(/\.js$/)) {
return;
}

it(file, () => {
const spec = require(path.resolve(redosDir, file));
const before = process.hrtime();
expect(spec).toRender(spec.html);
const elapsed = process.hrtime(before);
if (elapsed[0] > 0) {
const s = (elapsed[0] + elapsed[1] * 1e-9).toFixed(3);
fail(`took too long: ${s}s`);
}
});
});
});

0 comments on commit be27472

Please sign in to comment.