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
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## python-markdown2 2.5.6 (not yet released)

- [pull #732] Fix excessive CPU use in the inline HTML tokenizer on repeated unclosed tag fragments (#707)
- [pull #730] Fix `tables` extra splitting multi-backtick code spans at a leading pipe and merging cells after a code span containing literal backticks.
- [pull #729] Fix `tables` extra merging cells when a pipe directly follows a code span, as in compact rows like `|`-v`|verbose|`.
- [pull #725] Fix `tables` extra dropping escaped pipes at the end of header and body rows.
Expand Down
22 changes: 19 additions & 3 deletions lib/markdown2.py
Original file line number Diff line number Diff line change
Expand Up @@ -1171,7 +1171,24 @@ def _tag_is_closed(self, tag_name: str, text: str) -> bool:
return True

# check if number of open tags == number of close tags
if len(re.findall('<%s(?:.*?)>' % tag_name, text)) != text.count('</%s>' % tag_name):
# (a linear scan: `<tag.*?>` re-scans the rest of the line from every
# unclosed `<tag` and goes quadratic)
open_tag = '<%s' % tag_name
open_count = 0
line_end = -1
pos = text.find(open_tag)
while pos != -1:
if pos > line_end:
line_end = text.find('\n', pos)
if line_end == -1:
line_end = len(text)
end = text.find('>', pos, line_end)
if end == -1:
pos = text.find(open_tag, line_end + 1)
else:
open_count += 1
pos = text.find(open_tag, end + 1)
if open_count != text.count('</%s>' % tag_name):
return False

# check that close tag position is AFTER open tag
Expand Down Expand Up @@ -1349,8 +1366,7 @@ def _run_span_gamut(self, text: str) -> str:
(?:\w+) # tag name
(?: # attributes
\s+ # whitespace after tag
(?:[^\t<>"'=/]+:)?
[^<>"'=/]+= # attr name
[^\s<>"'=/][^<>"'=/]*= # attr name, can't start with whitespace
(?:"[^"]*?"|'[^']*?'|[^<>"'=/\s]+) # value, quoted or unquoted. If unquoted, no spaces allowed
)*
\s*/?>
Expand Down
18 changes: 18 additions & 0 deletions test/test_redos.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ def issue_668():
return 'a_b **x***y* c_d'


def issue_707_unclosed_tags():
# https://github.com/trentm/python-markdown2/issues/707
return '<p m="1"' * 15000


def issue_707_namespaced_attrs():
# https://github.com/trentm/python-markdown2/issues/707
return 'x <p' + ' a:b=1' * 40


def issue_707_spaced_attrs():
# https://github.com/trentm/python-markdown2/issues/707
return 'x <p' + ' a=1' * 40


# whack everything in a dict for easy lookup later on
CASES = {
fn.__name__: (fn, extras)
Expand All @@ -54,6 +69,9 @@ def issue_668():
(issue493, None),
(issue_633, None),
(issue_668, ['code-friendly']),
(issue_707_unclosed_tags, None),
(issue_707_namespaced_attrs, None),
(issue_707_spaced_attrs, None),
]
}

Expand Down