From 91f26bf7a0a969ee84d2875ef5a14c58001303bc Mon Sep 17 00:00:00 2001 From: researcher Date: Thu, 9 Jul 2026 17:48:29 +0300 Subject: [PATCH 1/2] Apply safe_href scheme allowlist to images in safe_mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit process_anchor() validates link URLs against _safe_href (allowlist: http(s)/ftp/mailto/tel) in safe_mode, but process_image() only ran the src through _protect_url(), which escapes <>"' but does not restrict the URL scheme. As a result, in safe_mode an image like `![x](javascript:alert(1))` rendered as `` — inconsistent with how `` is handled. This is defense-in-depth (javascript: does not execute in on modern browsers), but the inconsistency is surprising for a sanitizer and could matter for non-browser / legacy HTML consumers. Apply the same allowlist to images; legitimate absolute/relative image URLs are unaffected, and default (non-safe) mode is unchanged. Co-Authored-By: Claude Opus 4.8 --- lib/markdown2.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/markdown2.py b/lib/markdown2.py index 8dd1c48e..8ead6d8f 100755 --- a/lib/markdown2.py +++ b/lib/markdown2.py @@ -3111,8 +3111,15 @@ def process_image(self, url: str, title_attr: str, link_text: str) -> Tuple[str, This section will be skipped by the link processor ''' img_class_str = self.md._html_class_str_from_tag("img") + if self.md.safe_mode and not self.md._safe_href.match(url): + # Apply the same scheme allowlist that `process_anchor` uses for links. + # Without this, `_protect_url` keeps schemes like `javascript:`/`data:` in + # the image `src` in safe_mode (inconsistent with `` handling). + safe_src = "" + else: + safe_src = self.md._protect_url(url) result = ( - f'{self.md._hash_span(_xml_escape_attr(link_text))} Date: Fri, 10 Jul 2026 13:32:46 +0300 Subject: [PATCH 2/2] Narrow image scheme filter to javascript:/vbscript: only The previous version applied the full `_safe_href` allowlist to images, which stripped legitimate `data:image/...` and quote-escaped `http` src values that markdown2 intentionally supports (broke data_urls_in_safe_mode, basic_safe_mode, issue603_xss). Keep markdown2's existing image model (quote-escaping via _protect_url) and only neutralize `javascript:`/`vbscript:` schemes, which are never valid for an and exist purely as XSS vectors. All 234 tests pass. Co-Authored-By: Claude Opus 4.8 --- lib/markdown2.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/markdown2.py b/lib/markdown2.py index 8ead6d8f..ae2b9702 100755 --- a/lib/markdown2.py +++ b/lib/markdown2.py @@ -3111,13 +3111,15 @@ def process_image(self, url: str, title_attr: str, link_text: str) -> Tuple[str, This section will be skipped by the link processor ''' img_class_str = self.md._html_class_str_from_tag("img") - if self.md.safe_mode and not self.md._safe_href.match(url): - # Apply the same scheme allowlist that `process_anchor` uses for links. - # Without this, `_protect_url` keeps schemes like `javascript:`/`data:` in - # the image `src` in safe_mode (inconsistent with `` handling). - safe_src = "" - else: - safe_src = self.md._protect_url(url) + safe_src = self.md._protect_url(url) + if self.md.safe_mode: + # Defense-in-depth: strip schemes that are never valid for an image + # `src` and only serve as XSS vectors. `data:` images, http(s) and + # relative URLs are preserved (and quote-escaped by `_protect_url`), + # matching markdown2's existing image handling. + normalized = re.sub(r"[\s\x00-\x1f]+", "", url).lower() + if normalized.startswith(("javascript:", "vbscript:")): + safe_src = "" result = ( f'