diff --git a/CHANGES.md b/CHANGES.md index 23455420..cabaeb1f 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) +- [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 diff --git a/lib/markdown2.py b/lib/markdown2.py index 2da6ff29..18419ef0 100755 --- a/lib/markdown2.py +++ b/lib/markdown2.py @@ -1448,10 +1448,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 ) @@ -1736,7 +1743,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 @@ -3682,8 +3690,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 + + + a + b + + + + + `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 | ` | diff --git a/test/tm-cases/xss_code_spans_issue726.html b/test/tm-cases/xss_code_spans_issue726.html new file mode 100644 index 00000000..de01da01 --- /dev/null +++ b/test/tm-cases/xss_code_spans_issue726.html @@ -0,0 +1,7 @@ +

<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