Skip to content

Commit cca5ee6

Browse files
committed
fix: add image max depth
1 parent 0938fb7 commit cca5ee6

4 files changed

Lines changed: 61 additions & 1 deletion

File tree

‎src/mistune/core.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ def __init__(self, env: MutableMapping[str, Any]):
128128
self.src = ""
129129
self.tokens: List[Dict[str, Any]] = []
130130
self.in_image = False
131+
self.image_depth = 0
131132
self.in_link = False
132133
self.no_close_bracket_before: int = 0 # high-water mark for DoS mitigation
133134
self.no_link_before: int = 0 # high-water mark for failed balanced link candidates
@@ -145,6 +146,7 @@ def copy(self) -> "InlineState":
145146
"""Create a copy of current state."""
146147
state = self.__class__(self.env)
147148
state.in_image = self.in_image
149+
state.image_depth = self.image_depth
148150
state.in_link = self.in_link
149151
state.link_brackets = self.link_brackets
150152
return state

‎src/mistune/inline_parser.py‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
_REGEX_META_CHARS = set(r"()[]{}?*+|.^$")
2929
_CHARREF_PREFIX = re.compile(r"(#[0-9]{1,7};|#[xX][0-9a-fA-F]+;|[^\t\n\f <&#;]{1,32};)")
3030
DEFAULT_MAX_EMPHASIS_DEPTH = 20
31+
DEFAULT_MAX_IMAGE_DEPTH = 20
3132

3233
AUTO_EMAIL = (
3334
r"""<[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9]"""
@@ -85,11 +86,17 @@ class InlineParser(Parser[InlineState]):
8586
"linebreak",
8687
)
8788

88-
def __init__(self, hard_wrap: bool = False, max_emphasis_depth: int = DEFAULT_MAX_EMPHASIS_DEPTH) -> None:
89+
def __init__(
90+
self,
91+
hard_wrap: bool = False,
92+
max_emphasis_depth: int = DEFAULT_MAX_EMPHASIS_DEPTH,
93+
max_image_depth: int = DEFAULT_MAX_IMAGE_DEPTH,
94+
) -> None:
8995
super(InlineParser, self).__init__()
9096

9197
self.hard_wrap = hard_wrap
9298
self.max_emphasis_depth = max_emphasis_depth
99+
self.max_image_depth = max_image_depth
93100
self._fast_trigger_chars: Optional[Set[str]] = None
94101
self._fast_trigger_re: Optional[re.Pattern[str]] = None
95102
self._fast_trigger_re_chars: Optional[Tuple[str, ...]] = None
@@ -124,6 +131,9 @@ def parse_link(self, m: Match[str], state: InlineState) -> Optional[int]:
124131

125132
marker = m.group(0)
126133
is_image = marker[0] == "!"
134+
if is_image and self.max_image_depth > 0 and state.image_depth >= self.max_image_depth:
135+
state.append_token({"type": "text", "raw": marker})
136+
return pos
127137
if not is_image and state.in_link:
128138
state.append_token({"type": "text", "raw": marker})
129139
return pos
@@ -236,6 +246,7 @@ def __parse_link_token(
236246
new_state.src = text
237247
if is_image:
238248
new_state.in_image = True
249+
new_state.image_depth += 1
239250
token = {
240251
"type": "image",
241252
"children": self.render(new_state),

‎tests/test_misc.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,12 @@ def test_max_emphasis_depth_setting(self):
295295
md = mistune.Markdown(inline=mistune.InlineParser(max_emphasis_depth=7))
296296
self.assertEqual(md.inline.max_emphasis_depth, 7)
297297

298+
def test_max_image_depth_setting(self):
299+
self.assertEqual(mistune.InlineParser().max_image_depth, 20)
300+
self.assertEqual(mistune.create_markdown().inline.max_image_depth, 20)
301+
md = mistune.Markdown(inline=mistune.InlineParser(max_image_depth=7))
302+
self.assertEqual(md.inline.max_image_depth, 7)
303+
298304
def test_deeply_nested_block_quote_and_list(self):
299305
# Block quotes and lists each capped their own nesting at the limit but
300306
# not each other's, so alternating them recursed without bound. This

‎tests/test_security_inline.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@
99

1010

1111
class TestInlineParserSecurity(TestCase):
12+
def assert_max_token_depth(self, tokens, token_type, max_depth):
13+
stack = [(token, 0) for token in tokens]
14+
while stack:
15+
token, depth = stack.pop()
16+
if token["type"] == token_type:
17+
depth += 1
18+
self.assertLessEqual(depth, max_depth)
19+
for child in token.get("children", ()):
20+
stack.append((child, depth))
21+
1222
def test_link_title_redos_payload_returns_quickly(self):
1323
md = create_markdown()
1424
payload = '[x](y "' + "\\!" * 24 + ")"
@@ -72,3 +82,34 @@ def test_deep_emphasis_input_does_not_recurse_forever(self):
7282
self.assertLess(elapsed, DEADLINE)
7383
self.assertLessEqual(html.count("<strong>") + html.count("<em>"), 20)
7484
self.assertTrue(ast)
85+
86+
def test_deep_image_input_does_not_recurse_forever(self):
87+
text = "![" * 1000 + "a" + "](u)" * 1000
88+
89+
start = time.monotonic()
90+
create_markdown()(text)
91+
ast = create_markdown(renderer="ast")(text)
92+
elapsed = time.monotonic() - start
93+
94+
self.assertLess(elapsed, DEADLINE)
95+
self.assert_max_token_depth(ast, "image", 20)
96+
97+
def test_link_with_deep_image_input_does_not_recurse_forever(self):
98+
text = "[" + "![" * 1000 + "a" + "](u)" * 1000 + "](u)"
99+
100+
start = time.monotonic()
101+
create_markdown()(text)
102+
ast = create_markdown(renderer="ast")(text)
103+
elapsed = time.monotonic() - start
104+
105+
self.assertLess(elapsed, DEADLINE)
106+
self.assert_max_token_depth(ast, "image", 20)
107+
108+
def test_deep_link_input_does_not_recurse_forever(self):
109+
text = "[" * 1000 + "a" + "](u)" * 1000
110+
111+
start = time.monotonic()
112+
create_markdown(renderer="ast")(text)
113+
elapsed = time.monotonic() - start
114+
115+
self.assertLess(elapsed, DEADLINE)

0 commit comments

Comments
 (0)