Skip to content

Commit 5f4ce24

Browse files
authored
Merge pull request #442 from Gares95/harden-image-directive-src
2 parents 067f908 + 5afeaf6 commit 5f4ce24

3 files changed

Lines changed: 46 additions & 1 deletion

File tree

src/mistune/directives/image.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def render_block_image(
6262
height: Optional[str] = None,
6363
**attrs: Any,
6464
) -> str:
65-
img = '<img src="' + escape_text(src) + '"'
65+
img = '<img src="' + self.safe_url(src) + '"'
6666
style = ""
6767
if alt:
6868
img += ' alt="' + escape_text(alt) + '"'

tests/test_directives.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,20 @@ def test_include_missing_source(self):
9191
s = ".. include:: foo.txt"
9292
html = self.md(s)
9393
self.assertIn("Missing source file", html)
94+
95+
96+
class TestImageDirectiveSrc(BaseTestCase):
97+
md = create_markdown(escape=False, plugins=[RSTDirective([Image()])]) # type: ignore[list-item]
98+
99+
def test_harmful_src_blocked(self):
100+
html = self.md(".. image:: javascript:alert")
101+
self.assertIn('src="#harmful-link"', html)
102+
self.assertNotIn("javascript:alert", html)
103+
104+
def test_safe_src_preserved(self):
105+
html = self.md(".. image:: cat.png")
106+
self.assertIn('src="cat.png"', html)
107+
108+
def test_target_still_filtered(self):
109+
html = self.md(".. image:: cat.png\n :target: javascript:alert")
110+
self.assertNotIn("javascript:alert", html)

tests/test_misc.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,34 @@ def test_harmful_links(self):
4141
expected = '<p><a href="#harmful-link">h</a></p>'
4242
self.assertEqual(result.strip(), expected)
4343

44+
def test_harmful_links_variants(self):
45+
# entity-decoded, alternate-scheme, and reference-link forms are all
46+
# routed to the #harmful-link sentinel.
47+
for text in [
48+
"[h](javascript:alert)",
49+
"[h](&#x6a;avascript:alert)",
50+
"[h](javascript&colon;alert)",
51+
"[h](vbscript:msgbox)",
52+
"[h](file:///etc/passwd)",
53+
"[h](data:text/html,xss)",
54+
"[h](data:image/svg+xml,xss)",
55+
"[h][r]\n\n[r]: javascript:alert",
56+
]:
57+
result = mistune.html(text)
58+
self.assertIn('href="#harmful-link"', result, text)
59+
self.assertNotIn("javascript:alert", result, text)
60+
61+
def test_control_char_scheme_not_executable(self):
62+
# A control char or encoded colon inside the scheme is percent-encoded
63+
# by escape_url, so the rendered href is never an executable
64+
# javascript: scheme (regardless of the sentinel path).
65+
for text in [
66+
"[h](<java\tscript:alert>)",
67+
"[h](<java&#x09;script:alert>)",
68+
"[h](javascript%3Aalert)",
69+
]:
70+
self.assertNotIn("javascript:alert", mistune.html(text), text)
71+
4472
def test_ref_link(self):
4573
result = mistune.html("[link][h]\n\n[h]: /foo")
4674
expected = '<p><a href="/foo">link</a></p>'

0 commit comments

Comments
 (0)