From 9437eb287348f7d45c81ed1f6e89519bca76d012 Mon Sep 17 00:00:00 2001 From: saket3395 Date: Tue, 11 Aug 2026 18:46:41 +0530 Subject: [PATCH] fix: guard IndexError in html_block/heading terminator rules (gh-415) Input that ends on a blockquote marker while a table is open inside that quote (e.g. "> | a | b |\n> |---|---|\n>") raised IndexError: string index out of range. The table rule runs html_block and heading as terminator rules on the empty final line a trailing '>' produces, where pos = bMarks[startLine] + tShift[startLine] equals len(state.src). Both rules index state.src[pos] without guarding that boundary: html_block at 'if state.src[pos] != "<"' and heading at 'ch = state.src[pos]' (its pos >= maximum check runs on the next line, after the index). In markdown-it (JS) the equivalent charCodeAt(pos) returns NaN out of range instead of raising -- the port hazard from GH-190. Sibling terminator rules already defend against exactly this: hr.py and blockquote.py wrap the same index in try/except IndexError -> return False (added for GH-185 / GH-204). This applies the same guard to the two rules that were missed. Adds regression tests for both crash paths (html_block runs first with html enabled; heading is reached when html is disabled). --- markdown_it/rules_block/heading.py | 5 ++++- markdown_it/rules_block/html_block.py | 5 ++++- tests/test_fuzzer.py | 20 ++++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/markdown_it/rules_block/heading.py b/markdown_it/rules_block/heading.py index afcf9ed4..c58c1c54 100644 --- a/markdown_it/rules_block/heading.py +++ b/markdown_it/rules_block/heading.py @@ -19,7 +19,10 @@ def heading(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bo if state.is_code_block(startLine): return False - ch: str | None = state.src[pos] + try: + ch: str | None = state.src[pos] + except IndexError: + return False if ch != "#" or pos >= maximum: return False diff --git a/markdown_it/rules_block/html_block.py b/markdown_it/rules_block/html_block.py index 3d43f6ee..fe7e464e 100644 --- a/markdown_it/rules_block/html_block.py +++ b/markdown_it/rules_block/html_block.py @@ -44,7 +44,10 @@ def html_block(state: StateBlock, startLine: int, endLine: int, silent: bool) -> if not state.md.options.get("html", None): return False - if state.src[pos] != "<": + try: + if state.src[pos] != "<": + return False + except IndexError: return False lineText = state.src[pos:maximum] diff --git a/tests/test_fuzzer.py b/tests/test_fuzzer.py index 7286f8ea..42c6600b 100644 --- a/tests/test_fuzzer.py +++ b/tests/test_fuzzer.py @@ -23,3 +23,23 @@ def test_fuzzing(raw_input, expected): md = MarkdownIt() md.parse(raw_input) assert md.render(raw_input) == expected + + +# Input that ends on a blockquote marker while a table is open inside the quote +# used to raise ``IndexError: string index out of range`` from the terminator +# rules ``html_block`` and ``heading`` (gh-issue 415). ``table`` must be enabled +# for the terminator rules to run on that line. +GH_415_INPUT = "> | a | b |\n> |---|---|\n>" + + +def test_gh_415_table_in_blockquote_at_eof_html_block() -> None: + # html_block runs first, so with html enabled it is the rule that used to raise + md = MarkdownIt().enable("table") + md.render(GH_415_INPUT) # must not raise IndexError + + +def test_gh_415_table_in_blockquote_at_eof_heading() -> None: + # with html disabled, html_block bails at its options check and heading is + # the terminator rule that used to raise + md = MarkdownIt("commonmark", {"html": False}).enable("table") + md.render(GH_415_INPUT) # must not raise IndexError