Skip to content

Commit 5d1363c

Browse files
authored
Fix quadratic-time backtracking when a reference link has no URL
A malformed reference definition line, one with a label but no URL (just trailing whitespace after the colon), makes ReferenceProcessor's regex backtrack badly. The pattern had two adjacent [ ]* groups around an optional newline, both matching the same run of spaces, so for a string of n spaces there were n+1 ways to split them before the engine gave up and tried the next split. That turns markdown.markdown('[id]:' + ' ' * 50000) into an eight second call instead of a near-instant one, and it gets worse fast as the input grows. Rewrote the pattern so the leading run of spaces is consumed greedily by a single group, with the optional newline plus more spaces folded into one non-ambiguous alternative after it. Verified this produces identical matches (and identical groups) as the old pattern on the handful of valid reference-link shapes the tests already cover, and added a dedicated regression test that fails on unmodified master and passes with the fix. Fixes #798.
1 parent 0d6afd1 commit 5d1363c

3 files changed

Lines changed: 67 additions & 1 deletion

File tree

‎docs/changelog.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ See the [Contributing Guide](contributing.md) for details.
2727
* Fix an issue with excessive backtracking when matching inline code blocks (#1617).
2828
* `md_in_html` now honors tags added to `Markdown.block_level_elements` after
2929
the extension is loaded (#1246).
30+
* Fix quadratic-time regex backtracking in `ReferenceProcessor` when a link
31+
reference definition has no URL, e.g. a line consisting only of `[id]:`
32+
followed by many trailing spaces (#798).
3033

3134
## [3.10.3] - 2026-07-30
3235

‎markdown/blockprocessors.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,8 @@ def run(self, parent: etree.Element, blocks: list[str]) -> None:
577577
class ReferenceProcessor(BlockProcessor):
578578
""" Process link references. """
579579
RE = re.compile(
580-
r'^[ ]{0,3}\[([^\[\]]*)\]:[ ]*\n?[ ]*([^\s]+)[ ]*(?:\n[ ]*)?((["\'])(.*)\4[ ]*|\((.*)\)[ ]*)?$', re.MULTILINE
580+
r'^[ ]{0,3}\[([^\[\]]*)\]:[ ]*(?:\n[ ]*)?([^\s]+)[ ]*(?:\n[ ]*)?((["\'])(.*)\4[ ]*|\((.*)\)[ ]*)?$',
581+
re.MULTILINE
581582
)
582583

583584
def test(self, parent: etree.Element, block: str) -> bool:
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""
2+
Python Markdown
3+
4+
A Python implementation of John Gruber's Markdown.
5+
6+
Documentation: https://python-markdown.github.io/
7+
GitHub: https://github.com/Python-Markdown/markdown/
8+
PyPI: https://pypi.org/project/Markdown/
9+
10+
Started by Manfred Stienstra (http://www.dwerg.net/).
11+
Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
12+
Currently maintained by Waylan Limberg (https://github.com/waylan),
13+
Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
14+
15+
Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
16+
Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
17+
Copyright 2004 Manfred Stienstra (the original version)
18+
19+
License: BSD (see LICENSE.md for details).
20+
"""
21+
22+
from markdown.test_tools import TestCase
23+
24+
25+
class TestReferenceLinks(TestCase):
26+
27+
def test_reference_link(self):
28+
self.assertMarkdownRenders(
29+
'[Text][id]\n\n[id]: http://example.com',
30+
'<p><a href="http://example.com">Text</a></p>'
31+
)
32+
33+
def test_reference_link_split_across_lines(self):
34+
self.assertMarkdownRenders(
35+
'[Text][id]\n\n[id]:\nhttp://example.com',
36+
'<p><a href="http://example.com">Text</a></p>'
37+
)
38+
39+
def test_reference_link_with_title(self):
40+
self.assertMarkdownRenders(
41+
'[Text][id]\n\n[id]: http://example.com "Title"',
42+
'<p><a href="http://example.com" title="Title">Text</a></p>'
43+
)
44+
45+
def test_reference_link_title_on_own_line(self):
46+
self.assertMarkdownRenders(
47+
'[Text][id]\n\n[id]: http://example.com\n"Title"',
48+
'<p><a href="http://example.com" title="Title">Text</a></p>'
49+
)
50+
51+
def test_malformed_reference_with_long_run_of_spaces(self):
52+
"""
53+
A reference definition whose URL is missing (only trailing spaces
54+
after the colon) should still be treated as plain text, no matter
55+
how many trailing spaces there are.
56+
57+
See https://github.com/Python-Markdown/markdown/issues/798
58+
"""
59+
text = '[id]:' + (' ' * 50000)
60+
self.assertMarkdownRenders(
61+
text, f'<p>{text}</p>', expected_attrs={'references': {}}
62+
)

0 commit comments

Comments
 (0)