From 6fcf20be20557e9a931f6aed6e0daeb91606a095 Mon Sep 17 00:00:00 2001
From: aliassar
Date: Wed, 4 Nov 2026 19:48:31 +0100
Subject: [PATCH] Check image URL schemes in safe mode
safe_mode replaces unsafe link hrefs with '#', but image src values
were passed through unchecked, so  produced an
img tag with a javascript: src. Apply the same _safe_href check in
LinkProcessor.process_image. Base64 image data URLs stay allowed, but
only with a plain base64 payload, so a code span can't break out of
the src attribute.
Fixes #764
Co-Authored-By: Claude Opus 5.5
---
CHANGES.md | 1 +
lib/markdown2.py | 11 ++++++++
test/tm-cases/basic_safe_mode.html | 4 +--
test/tm-cases/image_safe_urls.html | 25 +++++++++++++++++
test/tm-cases/image_safe_urls.opts | 1 +
test/tm-cases/image_safe_urls.tags | 1 +
test/tm-cases/image_safe_urls.text | 28 +++++++++++++++++++
test/tm-cases/issue603_xss.html | 8 +++---
test/tm-cases/xss_code_spans_issue726.html | 4 +--
.../xss_smuggling_spans_in_image_attrs.html | 7 ++---
10 files changed, 77 insertions(+), 13 deletions(-)
create mode 100644 test/tm-cases/image_safe_urls.html
create mode 100644 test/tm-cases/image_safe_urls.opts
create mode 100644 test/tm-cases/image_safe_urls.tags
create mode 100644 test/tm-cases/image_safe_urls.text
diff --git a/CHANGES.md b/CHANGES.md
index afb0be54..a77bd029 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -27,6 +27,7 @@
- [pull #743] Add a `strip-syntax` option to the `header-ids` extra that leaves link URLs, image paths, inline HTML tags and emphasis markers out of header ids (#192)
- [pull #747] Fix `link-patterns` extra linking text inside Markdown links and images whose label wraps onto another line
- [pull #759] Fix `fenced-code-blocks` extra not recognizing code blocks whose language name contains `#` or `.`, such as `c#`, `f#` and `vb.net`
+- [pull #766] Fix `safe_mode` allowing `javascript:` and other unsafe URLs in image sources (#764)
## python-markdown2 2.5.5
diff --git a/lib/markdown2.py b/lib/markdown2.py
index e35bf5d2..0e13c689 100755
--- a/lib/markdown2.py
+++ b/lib/markdown2.py
@@ -3248,6 +3248,17 @@ def process_image(self, url: str, title_attr: str, link_text: str) -> Tuple[str,
2. The length of the opening HTML tag in the string. For `
` it's the whole string.
This section will be skipped by the link processor
'''
+ if self.md.safe_mode and not self.md._safe_href.match(url):
+ # same check as links, but keep allowing base64 image data URLs
+ data_url = self.md._data_url_re.match(url)
+ if not (
+ data_url
+ and (data_url.group('mime') or '').startswith('image/')
+ and data_url.group('token') == ';base64'
+ and re.fullmatch(r'[A-Za-z0-9+/=]*', data_url.group('data'))
+ ):
+ url = '#'
+
img_class_str = self.md._html_class_str_from_tag("img")
result = (
f'})

-)
+

-[HTML_REMOVED])
+
<img src="javascript:alert(1)"
diff --git a/test/tm-cases/image_safe_urls.html b/test/tm-cases/image_safe_urls.html
new file mode 100644
index 00000000..5517dfe2
--- /dev/null
+++ b/test/tm-cases/image_safe_urls.html
@@ -0,0 +1,25 @@
+Safe images
+
+
+
+
+
+
+
+Bad protocols
+
+
+
+
+
+
+
+
+
+Reference images
+
+
+
+
+
+
diff --git a/test/tm-cases/image_safe_urls.opts b/test/tm-cases/image_safe_urls.opts
new file mode 100644
index 00000000..ad487c04
--- /dev/null
+++ b/test/tm-cases/image_safe_urls.opts
@@ -0,0 +1 @@
+{"safe_mode": "escape"}
diff --git a/test/tm-cases/image_safe_urls.tags b/test/tm-cases/image_safe_urls.tags
new file mode 100644
index 00000000..02abe816
--- /dev/null
+++ b/test/tm-cases/image_safe_urls.tags
@@ -0,0 +1 @@
+safe_mode
diff --git a/test/tm-cases/image_safe_urls.text b/test/tm-cases/image_safe_urls.text
new file mode 100644
index 00000000..ebf267ba
--- /dev/null
+++ b/test/tm-cases/image_safe_urls.text
@@ -0,0 +1,28 @@
+# Safe images
+
+
+
+
+
+
+
+# Bad protocols
+
+)
+
+)
+
+)
+
+
+
+# Reference images
+
+![Safe ref][safe]
+
+![Unsafe ref][unsafe]
+
+[safe]: https://www.example.com/a.png
+[unsafe]: javascript:alert(1)
+
+//`)
diff --git a/test/tm-cases/issue603_xss.html b/test/tm-cases/issue603_xss.html
index 2293d586..9ca2bc7b 100644
--- a/test/tm-cases/issue603_xss.html
+++ b/test/tm-cases/issue603_xss.html
@@ -1,6 +1,6 @@
-//</code)
+
-
+
-
-//<code>)
+
+
diff --git a/test/tm-cases/xss_code_spans_issue726.html b/test/tm-cases/xss_code_spans_issue726.html
index de01da01..06f8edcf 100644
--- a/test/tm-cases/xss_code_spans_issue726.html
+++ b/test/tm-cases/xss_code_spans_issue726.html
@@ -1,6 +1,4 @@
-<x
><svg onload=alert(origin)
+<x
><svg onload=alert(origin)
Code spans still come out escaped once: a<b, <b> and C:\<folder>
diff --git a/test/tm-cases/xss_smuggling_spans_in_image_attrs.html b/test/tm-cases/xss_smuggling_spans_in_image_attrs.html
index c0837ef9..1b78de92 100644
--- a/test/tm-cases/xss_smuggling_spans_in_image_attrs.html
+++ b/test/tm-cases/xss_smuggling_spans_in_image_attrs.html
@@ -1,6 +1,6 @@

-![<code>" onerror="alert(1)//</code>]()
+

@@ -13,11 +13,10 @@
onerror=alert(origin) )
-"></code)
+
http://onclick=alert(origin)//
http://onclick=alert(origin)//[<codeClick me]()>
-">`)
+