Skip to content

Commit c7101fc

Browse files
committed
fix(renderer): block encoded unsafe URL schemes
1 parent 2f2449d commit c7101fc

2 files changed

Lines changed: 39 additions & 1 deletion

File tree

src/mistune/renderers/html.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import Any, ClassVar, Dict, Optional, Tuple, Literal
2+
from urllib.parse import unquote
23
from ..core import BaseRenderer, BlockState
34
from ..util import escape as escape_text
45
from ..util import safe_entity, striptags
@@ -14,6 +15,14 @@ class HTMLRenderer(BaseRenderer):
1415
"vbscript:",
1516
"file:",
1617
"data:",
18+
"feed:",
19+
"jar:",
20+
"livescript:",
21+
"mocha:",
22+
"ms-its:",
23+
"mk:",
24+
"res:",
25+
"view-source:",
1726
)
1827
GOOD_DATA_PROTOCOLS: ClassVar[Tuple[str, ...]] = (
1928
"data:image/gif;",
@@ -53,7 +62,7 @@ def safe_url(self, url: str) -> str:
5362
if self._allow_harmful_protocols is True:
5463
return escape_text(url)
5564

56-
_url = url.lower()
65+
_url = _unquote_url(url).lower()
5766
if self._allow_harmful_protocols and _url.startswith(tuple(self._allow_harmful_protocols)):
5867
return escape_text(url)
5968

@@ -151,3 +160,12 @@ def list(self, text: str, ordered: bool, **attrs: Any) -> str:
151160

152161
def list_item(self, text: str) -> str:
153162
return "<li>" + text + "</li>\n"
163+
164+
165+
def _unquote_url(url: str) -> str:
166+
for _ in range(3):
167+
decoded = unquote(url)
168+
if decoded == url:
169+
break
170+
url = decoded
171+
return url

tests/test_security_urls.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from unittest import TestCase
2+
3+
from mistune import create_markdown
4+
5+
6+
class TestSafeUrlSecurity(TestCase):
7+
def test_percent_encoded_harmful_url_scheme_is_blocked(self):
8+
for text in [
9+
"[h](javascript%3Aalert(1))",
10+
"[h](javascript%253Aalert(1))",
11+
"[h][r]\n\n[r]: javascript%3Aalert(1)",
12+
"![h](data%3Atext/html;base64,PHNjcmlwdD4=)",
13+
"[h](view-source:javascript:alert(1))",
14+
]:
15+
html = create_markdown()(text)
16+
self.assertIn("#harmful-link", html, text)
17+
18+
def test_safe_percent_encoded_data_image_is_allowed(self):
19+
html = create_markdown()("![h](data%3Aimage/png;base64,AAAA)")
20+
self.assertIn('src="data%3Aimage/png;base64,AAAA"', html)

0 commit comments

Comments
 (0)