Skip to content

Commit fe02f40

Browse files
committed
fix: improve nested bracket link input
1 parent 59c35e2 commit fe02f40

3 files changed

Lines changed: 38 additions & 4 deletions

File tree

src/mistune/core.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def __init__(self, env: MutableMapping[str, Any]):
130130
self.in_image = False
131131
self.in_link = False
132132
self.no_close_bracket_before: int = 0 # high-water mark for DoS mitigation
133+
self.no_link_before: int = 0 # high-water mark for failed balanced link candidates
133134
self.link_brackets: Dict[int, Tuple[str, Dict[int, int]]] = {}
134135

135136
def prepend_token(self, token: Dict[str, Any]) -> None:

src/mistune/inline_parser.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ def parse_link(self, m: Match[str], state: InlineState) -> Optional[int]:
125125
if not is_image and state.in_link:
126126
state.append_token({"type": "text", "raw": marker})
127127
return pos
128+
if not is_image and pos <= state.no_link_before:
129+
state.append_token({"type": "text", "raw": marker})
130+
return pos
128131

129132
text = None
130133
label, end_pos = parse_link_label(state.src, pos)
@@ -144,11 +147,13 @@ def parse_link(self, m: Match[str], state: InlineState) -> Optional[int]:
144147
text = label
145148

146149
assert text is not None
150+
body_end_pos = end_pos
147151

148-
if end_pos >= len(state.src) and label is None:
152+
has_nested_link = not is_image and self._contains_nested_link(text, state)
153+
if has_nested_link:
149154
return None
150-
151-
if not is_image and self._contains_nested_link(text, state):
155+
if end_pos >= len(state.src) and label is None:
156+
_mark_no_link_before(state, body_end_pos)
152157
return None
153158

154159
rules = ["codespan", "prec_auto_link", "prec_inline_html"]
@@ -175,10 +180,12 @@ def parse_link(self, m: Match[str], state: InlineState) -> Optional[int]:
175180
label = label2
176181

177182
if label is None:
183+
_mark_no_link_before(state, body_end_pos)
178184
return None
179185

180186
ref_links = state.env.get("ref_links")
181187
if not ref_links:
188+
_mark_no_link_before(state, body_end_pos)
182189
return None
183190

184191
key = unikey(label)
@@ -190,6 +197,7 @@ def parse_link(self, m: Match[str], state: InlineState) -> Optional[int]:
190197
token["label"] = label
191198
state.append_token(token)
192199
return end_pos
200+
_mark_no_link_before(state, body_end_pos)
193201
return None
194202

195203
def _contains_nested_link(self, text: str, state: InlineState) -> bool:
@@ -805,6 +813,11 @@ def _parse_link_text(state: InlineState, pos: int) -> Tuple[Optional[str], int]:
805813
return state.src[pos:close_pos], close_pos + 1
806814

807815

816+
def _mark_no_link_before(state: InlineState, end_pos: int) -> None:
817+
if end_pos > state.no_link_before:
818+
state.no_link_before = end_pos
819+
820+
808821
def _find_closing_bracket(state: InlineState, pos: int) -> Optional[int]:
809822
cache = state.link_brackets.get(id(state.src))
810823
if cache is not None and cache[0] is state.src:

tests/test_security_inline.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def test_link_title_redos_payload_returns_quickly(self):
1717
self.assertIn("[x](y", html)
1818

1919
def test_nested_bracket_link_input_does_not_recurse_forever(self):
20-
text = "[" * 200 + "x" + "]" * 200
20+
text = "[" * 8000 + "x" + "]" * 8000
2121

2222
start = time.monotonic()
2323
html = create_markdown()(text)
@@ -26,6 +26,26 @@ def test_nested_bracket_link_input_does_not_recurse_forever(self):
2626
self.assertLess(elapsed, 0.5)
2727
self.assertIn("[", html)
2828

29+
def test_failed_outer_bracket_preserves_nested_link(self):
30+
html = create_markdown()("[[x](https://example.com)]")
31+
32+
self.assertEqual('<p>[<a href="https://example.com">x</a>]</p>\n', html)
33+
34+
def test_failed_reference_after_nested_brackets_returns_quickly(self):
35+
text = "[" * 8000 + "x" + "]" * 8000 + "[missing]"
36+
37+
start = time.monotonic()
38+
html = create_markdown()(text)
39+
elapsed = time.monotonic() - start
40+
41+
self.assertLess(elapsed, 0.5)
42+
self.assertIn("[missing]", html)
43+
44+
def test_failed_reference_does_not_hide_following_inline_link(self):
45+
html = create_markdown()("[foo][bar](https://example.com)")
46+
47+
self.assertEqual('<p>[foo]<a href="https://example.com">bar</a></p>\n', html)
48+
2949
def test_unmatched_bracket_input_returns_quickly(self):
3050
text = "[" * 4000
3151

0 commit comments

Comments
 (0)