From 52effea407e1014d227cda9899fa9178187b4055 Mon Sep 17 00:00:00 2001 From: Crozzers Date: Mon, 7 Sep 2026 22:16:27 +0100 Subject: [PATCH 1/4] Fix optional semicolons on protocol separators passing _safe_href check --- lib/markdown2.py | 3 ++- test/tm-cases/xss_issue721.html | 1 + test/tm-cases/xss_issue721.opts | 1 + test/tm-cases/xss_issue721.text | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 test/tm-cases/xss_issue721.html create mode 100644 test/tm-cases/xss_issue721.opts create mode 100644 test/tm-cases/xss_issue721.text diff --git a/lib/markdown2.py b/lib/markdown2.py index bf7df1ef..1e17ae57 100755 --- a/lib/markdown2.py +++ b/lib/markdown2.py @@ -1602,7 +1602,8 @@ def _safe_href(self): # omitted ['"<>] for XSS reasons less_safe = r'#/\.!#$%&\(\)\+,/:;=\?@\[\]^`\{\}\|~' # html encoded colon in a URL still functions as a normal colon, so need to detect those - protocol_seperators = [':', ':', ':', ':'] + # semicolon at the end is optional in browsers - see #721 + protocol_seperators = [':', r':?', r':?', r':?'] # dot seperated hostname, optional port number, not followed by protocol seperator domain = r'(?:[{}]+(?:\.[{}]+)*)(?:(?Click me

diff --git a/test/tm-cases/xss_issue721.opts b/test/tm-cases/xss_issue721.opts new file mode 100644 index 00000000..54de31a8 --- /dev/null +++ b/test/tm-cases/xss_issue721.opts @@ -0,0 +1 @@ +{"safe_mode": "escape"} \ No newline at end of file diff --git a/test/tm-cases/xss_issue721.text b/test/tm-cases/xss_issue721.text new file mode 100644 index 00000000..ce92094e --- /dev/null +++ b/test/tm-cases/xss_issue721.text @@ -0,0 +1 @@ +[Click me](javascript:alert(origin)) \ No newline at end of file From 72de0f54e96d86571e85778334f65ae37e771352 Mon Sep 17 00:00:00 2001 From: Crozzers Date: Mon, 7 Sep 2026 22:47:26 +0100 Subject: [PATCH 2/4] Tweak _is_auto_link to reject unsafe URLs in safe mode --- lib/markdown2.py | 9 ++++++--- test/tm-cases/hash_html_blocks_orphaned_close_tags.html | 2 +- test/tm-cases/xss_issue721.html | 2 ++ test/tm-cases/xss_issue721.text | 4 +++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/markdown2.py b/lib/markdown2.py index 1e17ae57..af67d9c2 100755 --- a/lib/markdown2.py +++ b/lib/markdown2.py @@ -1397,9 +1397,12 @@ def _escape_special_chars(self, text: str) -> str: return ''.join(escaped) def _is_auto_link(self, text): - if ':' in text and self._auto_link_re.match(text): - return True - elif '@' in text and self._auto_email_link_re.match(text): + if ':' in text: + autolink_match = self._auto_link_re.match(text) + if autolink_match: + return self.safe_mode is None or self._safe_href.match(autolink_match.group(1)) + + if '@' in text and self._auto_email_link_re.match(text): return True return False diff --git a/test/tm-cases/hash_html_blocks_orphaned_close_tags.html b/test/tm-cases/hash_html_blocks_orphaned_close_tags.html index 372f357d..ff20b82f 100644 --- a/test/tm-cases/hash_html_blocks_orphaned_close_tags.html +++ b/test/tm-cases/hash_html_blocks_orphaned_close_tags.html @@ -6,6 +6,6 @@

-

http:/onmouseover=alert(origin)

+

<http:/onmouseover=alert(origin)>

-

diff --git a/test/tm-cases/xss_issue721.html b/test/tm-cases/xss_issue721.html index dd4a74fc..e0012a84 100644 --- a/test/tm-cases/xss_issue721.html +++ b/test/tm-cases/xss_issue721.html @@ -1 +1,3 @@

Click me

+ +

<http:|><x| oncontentvisibilityautostatechange=alert(origin) style=display:block;content-visibility:auto>

diff --git a/test/tm-cases/xss_issue721.text b/test/tm-cases/xss_issue721.text index ce92094e..43da8d75 100644 --- a/test/tm-cases/xss_issue721.text +++ b/test/tm-cases/xss_issue721.text @@ -1 +1,3 @@ -[Click me](javascript:alert(origin)) \ No newline at end of file +[Click me](javascript:alert(origin)) + + \ No newline at end of file From d01e5ae84ab79b45252c337cc7ff3994409eff01 Mon Sep 17 00:00:00 2001 From: Crozzers Date: Mon, 7 Sep 2026 23:14:31 +0100 Subject: [PATCH 3/4] Fix escaped HTML comments not having contents properly sanitized --- lib/markdown2.py | 8 ++++++-- test/tm-cases/escaped_html_in_safe_mode.html | 2 +- test/tm-cases/xss_issue721.html | 2 ++ test/tm-cases/xss_issue721.text | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/markdown2.py b/lib/markdown2.py index af67d9c2..d8a00942 100755 --- a/lib/markdown2.py +++ b/lib/markdown2.py @@ -1454,7 +1454,10 @@ def _is_comment(token): tokens.append(self._hash_span(self._sanitize_html(is_comment.group(3)))) elif self._is_unescaped_re.match(token) is None: # if the HTML is escaped then escape any special chars and add the token as-is - tokens.append(self._escape_special_chars(token)) + tokens.append( + # HTML can be snuck into escaped comment bodies - #721 + self._sanitize_html(self._escape_special_chars(token)) + ) else: tokens.append(self._hash_span(self._sanitize_html(token))) elif is_html_markup and is_code: @@ -1496,10 +1499,11 @@ def _sanitize_html(self, s: str) -> str: return self.html_removed_text elif self.safe_mode == "escape": replacements = [ - ('&', '&'), ('<', '<'), ('>', '>'), ] + # use a smart ampersand sub to avoid re-sanitizing stuff like `<` + s = _AMPERSAND_RE.sub('&', s) for before, after in replacements: s = s.replace(before, after) return s diff --git a/test/tm-cases/escaped_html_in_safe_mode.html b/test/tm-cases/escaped_html_in_safe_mode.html index ddb13060..7a78a535 100644 --- a/test/tm-cases/escaped_html_in_safe_mode.html +++ b/test/tm-cases/escaped_html_in_safe_mode.html @@ -1,3 +1,3 @@

<abc> -<abc> +<abc> <why?

diff --git a/test/tm-cases/xss_issue721.html b/test/tm-cases/xss_issue721.html index e0012a84..fc596d07 100644 --- a/test/tm-cases/xss_issue721.html +++ b/test/tm-cases/xss_issue721.html @@ -1,3 +1,5 @@

Click me

<http:|><x| oncontentvisibilityautostatechange=alert(origin) style=display:block;content-visibility:auto>

+ +

<!--<img src onerror=alert(origin)//>-->

diff --git a/test/tm-cases/xss_issue721.text b/test/tm-cases/xss_issue721.text index 43da8d75..4d8e8fcd 100644 --- a/test/tm-cases/xss_issue721.text +++ b/test/tm-cases/xss_issue721.text @@ -1,3 +1,5 @@ [Click me](javascript:alert(origin)) - \ No newline at end of file + + +\ From 350e34c755c3c1742b82717331d5ee08dc3af7fe Mon Sep 17 00:00:00 2001 From: Crozzers Date: Mon, 7 Sep 2026 23:20:43 +0100 Subject: [PATCH 4/4] Update changelog --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 21fb4ca3..8f027a0e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ - [pull #713] Fix `header-ids` extra generating duplicate ids when a suffixed id collides with another header (#661) - [pull #705] XSS fixes in links, images, and more - [pull #720] Add `wiki-links` extra for `[[Page Name]]` style links (#221) +- [pull #722] Harden URL safety checks and sanitization in safe mode (#721) ## python-markdown2 2.5.5