From 3a44ef8e16ea98a3a3dcd2ebb0427e5afc507632 Mon Sep 17 00:00:00 2001 From: LuisFigueroaG Date: Mon, 12 Oct 2026 22:28:44 -0300 Subject: [PATCH 1/2] Don't double escape incomplete tags in code blocks in safe mode In safe mode, incomplete tags like the `<3` in `i<3` are swapped for a literal `<` before the block gamut runs. Code blocks then encode the `&` again, so `i<3` inside an indented or fenced code block came out as `i&lt;3`. Hash the `<` instead, the same way complete tags are hashed, so code blocks leave it alone. The hash is also added to the code table so header ids and TOC entries keep using the plain text. Fixes #240 --- CHANGES.md | 1 + lib/markdown2.py | 9 ++++--- ...e_mode_incomplete_tags_in_code_blocks.html | 27 +++++++++++++++++++ ...e_mode_incomplete_tags_in_code_blocks.opts | 1 + ...e_mode_incomplete_tags_in_code_blocks.text | 21 +++++++++++++++ 5 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 test/tm-cases/safe_mode_incomplete_tags_in_code_blocks.html create mode 100644 test/tm-cases/safe_mode_incomplete_tags_in_code_blocks.opts create mode 100644 test/tm-cases/safe_mode_incomplete_tags_in_code_blocks.text diff --git a/CHANGES.md b/CHANGES.md index 23455420..1c10de34 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,7 @@ - [pull #728] Fix XSS from zero-padded colon entities in link URLs (#726) - [pull #734] Fix empty list items being parsed as nested lists (#167) - [pull #735] Fix `smarty-pants` extra converting quotes inside inline HTML tags and comments (#150) +- Fix `<` in code blocks being escaped twice in safe mode (#240) ## python-markdown2 2.5.5 diff --git a/lib/markdown2.py b/lib/markdown2.py index 2da6ff29..fea3256f 100755 --- a/lib/markdown2.py +++ b/lib/markdown2.py @@ -1670,7 +1670,7 @@ def header_id_from_text(self, None to not have an id attribute and to exclude this header from the TOC (if the "toc" extra is specified). """ - header_id = _slugify(html.unescape(text)) + header_id = _slugify(html.unescape(self._unescape_special_chars(text))) if prefix and isinstance(prefix, str): header_id = prefix + '-' + header_id @@ -2327,10 +2327,13 @@ def _encode_incomplete_tags(self, text: str) -> str: def incomplete_tags_sub(match): text = match.group() + # hash the `<` so that code blocks don't escape it a second time + lt_hash = self._hash_span('<') + self._code_table['<'] = lt_hash # ensure that we handle escaped incomplete tags properly by consuming and replacing the escapes if not self._is_unescaped_re.match(text): - text = text.replace('\\<', '<') - return text.replace('<', '<') + text = text.replace('\\<', lt_hash) + return text.replace('<', lt_hash) text = self._incomplete_tags_re.sub(incomplete_tags_sub, text) diff --git a/test/tm-cases/safe_mode_incomplete_tags_in_code_blocks.html b/test/tm-cases/safe_mode_incomplete_tags_in_code_blocks.html new file mode 100644 index 00000000..ac0e08aa --- /dev/null +++ b/test/tm-cases/safe_mode_incomplete_tags_in_code_blocks.html @@ -0,0 +1,27 @@ +

Comparing a<b

+ +

Start of test.

+ +
for (i=0; i<3; i++) {
+    printf("Hello, world.");
+}
+
+ +
+

This is a quote

+
+ +
if (a<b && b<c) {}
+
+ + + +

These are still escaped: a<b and <c

+ +
Literal entity: &lt;div
+
diff --git a/test/tm-cases/safe_mode_incomplete_tags_in_code_blocks.opts b/test/tm-cases/safe_mode_incomplete_tags_in_code_blocks.opts new file mode 100644 index 00000000..998d391b --- /dev/null +++ b/test/tm-cases/safe_mode_incomplete_tags_in_code_blocks.opts @@ -0,0 +1 @@ +{'safe_mode': 'escape', 'extras': ['fenced-code-blocks', 'header-ids']} \ No newline at end of file diff --git a/test/tm-cases/safe_mode_incomplete_tags_in_code_blocks.text b/test/tm-cases/safe_mode_incomplete_tags_in_code_blocks.text new file mode 100644 index 00000000..1f26a623 --- /dev/null +++ b/test/tm-cases/safe_mode_incomplete_tags_in_code_blocks.text @@ -0,0 +1,21 @@ +# Comparing a This is a quote + +``` +if (a Date: Mon, 12 Oct 2026 22:30:16 -0300 Subject: [PATCH 2/2] Add the PR number to the change note --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1c10de34..dd462bb2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,7 +21,7 @@ - [pull #728] Fix XSS from zero-padded colon entities in link URLs (#726) - [pull #734] Fix empty list items being parsed as nested lists (#167) - [pull #735] Fix `smarty-pants` extra converting quotes inside inline HTML tags and comments (#150) -- Fix `<` in code blocks being escaped twice in safe mode (#240) +- [pull #738] Fix `<` in code blocks being escaped twice in safe mode (#240) ## python-markdown2 2.5.5