Skip to content

Commit be27472

Browse files
committed
Improve worst-case performance of inline.text regex
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 <[email protected]>
1 parent ba1de1e commit be27472

File tree

5 files changed

+36
-8
lines changed

5 files changed

+36
-8
lines changed

lib/marked.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ var inline = {
546546
code: /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,
547547
br: /^( {2,}|\\)\n(?!\s*$)/,
548548
del: noop,
549-
text: /^(`+|[^`])[\s\S]*?(?=[\\<!\[`*]|\b_| {2,}\n|$)/
549+
text: /^(`+|[^`])(?:[\s\S]*?(?:(?=[\\<!\[`*]|\b_|$)|[^ ](?= {2,}\n))|(?= {2,}\n))/
550550
};
551551

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

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

631628
inline.breaks = merge({}, inline.gfm, {
632629
br: edit(inline.br).replace('{2,}', '*').getRegex(),
633-
text: edit(inline.gfm.text).replace('{2,}', '*').getRegex()
630+
text: edit(inline.gfm.text).replace(/\{2,\}/g, '*').getRegex()
634631
});
635632

636633
/**

test/redos/quadratic_br.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
markdown: `a${' '.repeat(50000)}`,
3+
html: `<p>a${' '.repeat(50000)}</p>`
4+
};

test/redos/quadratic_email.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
markdown: 'a'.repeat(50000),
3+
html: `<p>${'a'.repeat(50000)}</p>`
4+
};

test/specs/gfm/gfm.0.28.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,7 @@
141141
"section": "Autolinks",
142142
"html": "<p><a href=\"mailto:[email protected]\">[email protected]</a></p>\n<p><a href=\"mailto:[email protected]\">[email protected]</a>.</p>\n<p>[email protected]</p>\n<p>[email protected]_</p>",
143143
144-
"example": 607,
145-
"shouldFail": true
144+
"example": 607
146145
},
147146
{
148147
"section": "Disallowed Raw HTML",

test/specs/redos-spec.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const path = require('path');
2+
const fs = require('fs');
3+
4+
const redosDir = path.resolve(__dirname, '../redos');
5+
6+
describe('ReDOS tests', () => {
7+
const files = fs.readdirSync(redosDir);
8+
files.forEach(file => {
9+
if (!file.match(/\.js$/)) {
10+
return;
11+
}
12+
13+
it(file, () => {
14+
const spec = require(path.resolve(redosDir, file));
15+
const before = process.hrtime();
16+
expect(spec).toRender(spec.html);
17+
const elapsed = process.hrtime(before);
18+
if (elapsed[0] > 0) {
19+
const s = (elapsed[0] + elapsed[1] * 1e-9).toFixed(3);
20+
fail(`took too long: ${s}s`);
21+
}
22+
});
23+
});
24+
});

0 commit comments

Comments
 (0)