From fcf8bbd5948dbd409aa394459fd6a7024b438fe9 Mon Sep 17 00:00:00 2001 From: LuisFigueroaG Date: Wed, 7 Oct 2026 13:19:42 -0300 Subject: [PATCH 1/3] Fix XSS from code spans in safe mode Code spans are protected from HTML sanitization before the main conversion, but the span gamut doesn't always pair backticks the same way (a tag spanning multiple lines changes the pairing), so text that was skipped as "code" could be output as raw HTML. Replace any `<` in those protected spans with a hash that unescapes to `<`, so they're safe however they end up being parsed while real code spans still only get escaped once. Header ids and pygments highlighting unescape the hash first so their output doesn't change. Fixes #726 --- CHANGES.md | 1 + lib/markdown2.py | 17 ++++++++++++----- ...de_code_spans_in_fenced_code_with_lexer.html | 4 ++++ ...de_code_spans_in_fenced_code_with_lexer.opts | 1 + ...de_code_spans_in_fenced_code_with_lexer.tags | 1 + ...de_code_spans_in_fenced_code_with_lexer.text | 3 +++ test/tm-cases/xss_code_spans_issue726.html | 7 +++++++ test/tm-cases/xss_code_spans_issue726.opts | 1 + test/tm-cases/xss_code_spans_issue726.text | 7 +++++++ 9 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 test/tm-cases/safe_mode_code_spans_in_fenced_code_with_lexer.html create mode 100644 test/tm-cases/safe_mode_code_spans_in_fenced_code_with_lexer.opts create mode 100644 test/tm-cases/safe_mode_code_spans_in_fenced_code_with_lexer.tags create mode 100644 test/tm-cases/safe_mode_code_spans_in_fenced_code_with_lexer.text create mode 100644 test/tm-cases/xss_code_spans_issue726.html create mode 100644 test/tm-cases/xss_code_spans_issue726.opts create mode 100644 test/tm-cases/xss_code_spans_issue726.text diff --git a/CHANGES.md b/CHANGES.md index 8911b5cb..cfeed572 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,7 @@ - [pull #720] Add `wiki-links` extra for `[[Page Name]]` style links (#221) - [pull #722] Harden URL safety checks and sanitization in safe mode (#721) - [pull #728] Fix XSS from zero-padded colon entities in link URLs (#726) +- Fix XSS from code spans that the span gamut doesn't treat as code spans in safe mode (#726) ## python-markdown2 2.5.5 diff --git a/lib/markdown2.py b/lib/markdown2.py index b3c29847..46ea8a28 100755 --- a/lib/markdown2.py +++ b/lib/markdown2.py @@ -1438,10 +1438,17 @@ def _is_comment(token): return re.match(r'()', token) # protect raw code spans from processing, as they can often contain anything that looks like HTML and - # trips up the regex. These are encoded and processed later on anyway + # trips up the regex. These are encoded and processed later on anyway. + # The span gamut doesn't always agree that these are code spans + # (eg: a tag spanning lines changes how backticks pair up), so swap + # any `<` for a hash that unescapes to `<`. Nothing in here can + # then come out as raw HTML (#726), and real code spans still only + # get escaped once. code_hashes = {} + lt_hash = _hash_text('<') + self._code_table['<'] = lt_hash text = self._code_span_re.sub( - lambda m: self._hash_span(m.string[m.start(): m.end()], code_hashes), + lambda m: self._hash_span(m.group(0).replace('<', lt_hash), code_hashes), text ) @@ -1726,7 +1733,8 @@ def _h_sub(self, match: re.Match[str]) -> str: n = min(n + demote_headers, 6) header_id_attr = "" if "header-ids" in self.extras: - header_id = self.header_id_from_text(header_group, + header_id = self.header_id_from_text( + self._unescape_special_chars(header_group), self.extras["header-ids"].get('prefix'), n) if header_id: header_id_attr = ' id="%s"' % header_id @@ -3672,8 +3680,7 @@ def _code_block_with_lexer_sub( formatter_opts = self.md.extras['fenced-code-blocks'] or {} def unhash_code(codeblock): - for key, sanitized in list(self.md.html_spans.items()): - codeblock = codeblock.replace(key, sanitized) + codeblock = self.md._unhash_html_spans(codeblock, code=True) replacements = [ ("&", "&"), ("<", "<"), diff --git a/test/tm-cases/safe_mode_code_spans_in_fenced_code_with_lexer.html b/test/tm-cases/safe_mode_code_spans_in_fenced_code_with_lexer.html new file mode 100644 index 00000000..d9db860b --- /dev/null +++ b/test/tm-cases/safe_mode_code_spans_in_fenced_code_with_lexer.html @@ -0,0 +1,4 @@ +
+
print `a<b`
+
+
diff --git a/test/tm-cases/safe_mode_code_spans_in_fenced_code_with_lexer.opts b/test/tm-cases/safe_mode_code_spans_in_fenced_code_with_lexer.opts new file mode 100644 index 00000000..d5258749 --- /dev/null +++ b/test/tm-cases/safe_mode_code_spans_in_fenced_code_with_lexer.opts @@ -0,0 +1 @@ +{'safe_mode': 'escape', 'extras': ['fenced-code-blocks']} \ No newline at end of file diff --git a/test/tm-cases/safe_mode_code_spans_in_fenced_code_with_lexer.tags b/test/tm-cases/safe_mode_code_spans_in_fenced_code_with_lexer.tags new file mode 100644 index 00000000..e2282d2f --- /dev/null +++ b/test/tm-cases/safe_mode_code_spans_in_fenced_code_with_lexer.tags @@ -0,0 +1 @@ +fenced-code-blocks pygments \ No newline at end of file diff --git a/test/tm-cases/safe_mode_code_spans_in_fenced_code_with_lexer.text b/test/tm-cases/safe_mode_code_spans_in_fenced_code_with_lexer.text new file mode 100644 index 00000000..4131fbb8 --- /dev/null +++ b/test/tm-cases/safe_mode_code_spans_in_fenced_code_with_lexer.text @@ -0,0 +1,3 @@ +```python +print `a<x><svg onload=alert(origin)

+ +

Code spans still come out escaped once: a<b, <b> and C:\<folder>

+ +

Header with a<b in it

diff --git a/test/tm-cases/xss_code_spans_issue726.opts b/test/tm-cases/xss_code_spans_issue726.opts new file mode 100644 index 00000000..4cbd41b4 --- /dev/null +++ b/test/tm-cases/xss_code_spans_issue726.opts @@ -0,0 +1 @@ +{"safe_mode": "escape", "extras": ["header-ids"]} \ No newline at end of file diff --git a/test/tm-cases/xss_code_spans_issue726.text b/test/tm-cases/xss_code_spans_issue726.text new file mode 100644 index 00000000..11c6c16c --- /dev/null +++ b/test/tm-cases/xss_code_spans_issue726.text @@ -0,0 +1,7 @@ +`)>`) + +Code spans still come out escaped once: `a` and `C:\` + +# Header with `a Date: Mon, 12 Oct 2026 22:19:44 -0300 Subject: [PATCH 2/3] Add the tables case from #726 --- .../xss_code_spans_in_tables_issue726.html | 14 ++++++++++++++ .../xss_code_spans_in_tables_issue726.opts | 1 + .../xss_code_spans_in_tables_issue726.text | 3 +++ 3 files changed, 18 insertions(+) create mode 100644 test/tm-cases/xss_code_spans_in_tables_issue726.html create mode 100644 test/tm-cases/xss_code_spans_in_tables_issue726.opts create mode 100644 test/tm-cases/xss_code_spans_in_tables_issue726.text diff --git a/test/tm-cases/xss_code_spans_in_tables_issue726.html b/test/tm-cases/xss_code_spans_in_tables_issue726.html new file mode 100644 index 00000000..42603252 --- /dev/null +++ b/test/tm-cases/xss_code_spans_in_tables_issue726.html @@ -0,0 +1,14 @@ + + + + + + + + + + + + + +
ab
`x<svg onload=alert(origin)>`
diff --git a/test/tm-cases/xss_code_spans_in_tables_issue726.opts b/test/tm-cases/xss_code_spans_in_tables_issue726.opts new file mode 100644 index 00000000..3da78cd0 --- /dev/null +++ b/test/tm-cases/xss_code_spans_in_tables_issue726.opts @@ -0,0 +1 @@ +{"safe_mode": "escape", "extras": ["tables"]} diff --git a/test/tm-cases/xss_code_spans_in_tables_issue726.text b/test/tm-cases/xss_code_spans_in_tables_issue726.text new file mode 100644 index 00000000..41e02416 --- /dev/null +++ b/test/tm-cases/xss_code_spans_in_tables_issue726.text @@ -0,0 +1,3 @@ +| a | b | +|---|---| +| `x | ` | From 1064211e272fb1723d8a347b3fb2a5e04222cc42 Mon Sep 17 00:00:00 2001 From: LuisFigueroaG Date: Mon, 12 Oct 2026 22:20:01 -0300 Subject: [PATCH 3/3] 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 d66b9597..cabaeb1f 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 XSS from code spans that the span gamut doesn't treat as code spans in safe mode (#726) +- [pull #737] Fix XSS from code spans that the span gamut doesn't treat as code spans in safe mode (#726) ## python-markdown2 2.5.5