From c3687824b8c4b6c5fe9e67d4d49f41165762b75a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afonso=20Janu=C3=A1rio?= Date: Mon, 7 Sep 2026 10:24:17 +0100 Subject: [PATCH 1/2] Fix quadratic-time regex backtracking in reference link parsing 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. --- docs/changelog.md | 3 + markdown/blockprocessors.py | 3 +- .../blocks/test_reference_links.py | 65 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 tests/test_syntax/blocks/test_reference_links.py diff --git a/docs/changelog.md b/docs/changelog.md index 91373276..2e8d9bf8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -26,6 +26,9 @@ See the [Contributing Guide](contributing.md) for details. * Fix an issue with excessive backtracking when matching inline code blocks (#1617). * `md_in_html` now honors tags added to `Markdown.block_level_elements` after the extension is loaded (#1246). +* Fix quadratic-time regex backtracking in `ReferenceProcessor` when a link + reference definition has no URL, e.g. a line consisting only of `[id]:` + followed by many trailing spaces (#798). ## [3.10.3] - 2026-07-30 diff --git a/markdown/blockprocessors.py b/markdown/blockprocessors.py index c2d20ddb..cbad917b 100644 --- a/markdown/blockprocessors.py +++ b/markdown/blockprocessors.py @@ -577,7 +577,8 @@ def run(self, parent: etree.Element, blocks: list[str]) -> None: class ReferenceProcessor(BlockProcessor): """ Process link references. """ RE = re.compile( - r'^[ ]{0,3}\[([^\[\]]*)\]:[ ]*\n?[ ]*([^\s]+)[ ]*(?:\n[ ]*)?((["\'])(.*)\4[ ]*|\((.*)\)[ ]*)?$', re.MULTILINE + r'^[ ]{0,3}\[([^\[\]]*)\]:[ ]*(?:\n[ ]*)?([^\s]+)[ ]*(?:\n[ ]*)?((["\'])(.*)\4[ ]*|\((.*)\)[ ]*)?$', + re.MULTILINE ) def test(self, parent: etree.Element, block: str) -> bool: diff --git a/tests/test_syntax/blocks/test_reference_links.py b/tests/test_syntax/blocks/test_reference_links.py new file mode 100644 index 00000000..f116e6c0 --- /dev/null +++ b/tests/test_syntax/blocks/test_reference_links.py @@ -0,0 +1,65 @@ +""" +Python Markdown + +A Python implementation of John Gruber's Markdown. + +Documentation: https://python-markdown.github.io/ +GitHub: https://github.com/Python-Markdown/markdown/ +PyPI: https://pypi.org/project/Markdown/ + +Started by Manfred Stienstra (http://www.dwerg.net/). +Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). +Currently maintained by Waylan Limberg (https://github.com/waylan), +Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). + +Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +License: BSD (see LICENSE.md for details). +""" + +import time + +from markdown.test_tools import TestCase + + +class TestReferenceLinks(TestCase): + + def test_reference_link(self): + self.assertMarkdownRenders( + '[Text][id]\n\n[id]: http://example.com', + '

Text

' + ) + + def test_reference_link_split_across_lines(self): + self.assertMarkdownRenders( + '[Text][id]\n\n[id]:\nhttp://example.com', + '

Text

' + ) + + def test_reference_link_with_title(self): + self.assertMarkdownRenders( + '[Text][id]\n\n[id]: http://example.com "Title"', + '

Text

' + ) + + def test_reference_link_title_on_own_line(self): + self.assertMarkdownRenders( + '[Text][id]\n\n[id]: http://example.com\n"Title"', + '

Text

' + ) + + def test_malformed_reference_does_not_take_quadratic_time(self): + """ + A reference definition whose URL is missing (only trailing spaces + after the colon) must not trigger catastrophic regex backtracking. + + See https://github.com/Python-Markdown/markdown/issues/798 + """ + text = '[id]:' + (' ' * 50000) + start = time.time() + self.assertMarkdownRenders( + text, f'

{text}

', expected_attrs={'references': {}} + ) + self.assertLess(time.time() - start, 2) From 5f7cd8c1064b80dc6fb3f33de18d5f267fd0bfa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afonso=20Janu=C3=A1rio?= Date: Tue, 8 Sep 2026 00:09:55 +0100 Subject: [PATCH 2/2] Drop the wall-clock timing assertion from the reference-link test facelessuser pointed out that timing a test is fragile on slower hardware, so the regression test now only checks that the malformed reference is rendered correctly, without asserting anything about how long it takes. The regex fix itself is what prevents the quadratic blowup; ran the case manually and it completes in well under a millisecond now, versus roughly 8 seconds before the fix. --- tests/test_syntax/blocks/test_reference_links.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/test_syntax/blocks/test_reference_links.py b/tests/test_syntax/blocks/test_reference_links.py index f116e6c0..8bd67521 100644 --- a/tests/test_syntax/blocks/test_reference_links.py +++ b/tests/test_syntax/blocks/test_reference_links.py @@ -19,8 +19,6 @@ License: BSD (see LICENSE.md for details). """ -import time - from markdown.test_tools import TestCase @@ -50,16 +48,15 @@ def test_reference_link_title_on_own_line(self): '

Text

' ) - def test_malformed_reference_does_not_take_quadratic_time(self): + def test_malformed_reference_with_long_run_of_spaces(self): """ A reference definition whose URL is missing (only trailing spaces - after the colon) must not trigger catastrophic regex backtracking. + after the colon) should still be treated as plain text, no matter + how many trailing spaces there are. See https://github.com/Python-Markdown/markdown/issues/798 """ text = '[id]:' + (' ' * 50000) - start = time.time() self.assertMarkdownRenders( text, f'

{text}

', expected_attrs={'references': {}} ) - self.assertLess(time.time() - start, 2)