Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mdit_py_plugins/colon_fence.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def _render(

return (
"<pre><code"
+ (f' class="block-{block_name}" ' if block_name else "")
+ (f' class="block-{escapeHtml(block_name)}" ' if block_name else "")
+ ">"
+ content
+ "</code></pre>\n"
Expand Down
4 changes: 3 additions & 1 deletion mdit_py_plugins/tasklists/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from uuid import uuid4

from markdown_it import MarkdownIt
from markdown_it.common.utils import escapeHtml
from markdown_it.rules_core import StateCore
from markdown_it.token import Token

Expand Down Expand Up @@ -132,7 +133,8 @@ def end_label() -> Token:
def after_label(content: str, checkbox_id: str) -> Token:
token = Token("html_inline", "", 0)
token.content = (
f'<label class="task-list-item-label" for="{checkbox_id}">{content}</label>'
'<label class="task-list-item-label" '
f'for="{checkbox_id}">{escapeHtml(content)}</label>'
)
token.attrs = {"for": checkbox_id}
return token
Expand Down
33 changes: 33 additions & 0 deletions tests/test_colon_fence.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,36 @@ def test_plugin_parse(data_regression):
)
)
data_regression.check([t.as_dict() for t in tokens])


@pytest.mark.parametrize(
"source,expected",
[
pytest.param(
':::a"onmouseover="alert(1)\nx\n:::\n',
'<pre><code class="block-a&quot;onmouseover=&quot;alert(1)" >x\n'
"</code></pre>\n",
id="raw-quote",
),
pytest.param(
":::a&quot;&gt;&lt;xss&gt;\nx\n:::\n",
'<pre><code class="block-a&quot;&gt;&lt;xss&gt;" >x\n</code></pre>\n',
id="entity-encoded",
),
pytest.param(
# benign: same attribute value as before, now well-formed
":::a&b\nx\n:::\n",
'<pre><code class="block-a&amp;b" >x\n</code></pre>\n',
id="ampersand",
),
],
)
def test_block_name_is_escaped(source, expected):
"""The block name must not break out of the ``class`` attribute (#143).

With ``html=False`` the output must not carry markup from the source,
but the name used to be interpolated into the attribute unescaped
(and the entity-encoded form was unescaped first, then interpolated).
"""
md = MarkdownIt("commonmark", {"html": False}).use(colon_fence_plugin)
assert md.render(source) == expected
47 changes: 47 additions & 0 deletions tests/test_tasklists.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from pathlib import Path
import re
from textwrap import dedent

from markdown_it import MarkdownIt
Expand Down Expand Up @@ -32,3 +33,49 @@ def test_all(line, title, input, expected):
text = md.render(input)
print(text)
assert text.rstrip() == expected.rstrip()


@pytest.mark.parametrize(
"source,label",
[
pytest.param(
"- [ ] <img src=x onerror=alert(1)>\n",
" &lt;img src=x onerror=alert(1)&gt;",
id="raw-html",
),
pytest.param("- [x] a & b\n", " a &amp; b", id="ampersand"),
pytest.param("- [ ] plain task\n", " plain task", id="plain"),
],
)
def test_label_after_is_escaped(source, label):
"""The ``label_after`` label must not carry raw HTML from the source (#143).

The label is built from the item's source text and emitted as an
``html_inline`` token, so it used to bypass ``html=False``.
"""
md = MarkdownIt("commonmark", {"html": False}).use(
tasklists_plugin, label=True, label_after=True
)
text = re.sub(r"task-item-[0-9a-f-]+", "task-item-ID", md.render(source))
checked = ' checked="checked"' if source.startswith("- [x]") else ""
assert text == (
'<ul class="contains-task-list">\n'
'<li class="task-list-item"><input class="task-list-item-checkbox"'
f'{checked} disabled="disabled" type="checkbox" id="task-item-ID">'
f'<label class="task-list-item-label" for="task-item-ID">{label}</label>'
"</li>\n</ul>\n"
)


def test_label_after_entity_is_shown_literally():
"""The ``label_after`` label is the item's raw source text, escaped (#143).

So a source entity is shown as written (``&amp;``), not decoded.
Only the label is asserted: the inline children before the last one
are also rendered outside the label, which is a separate, existing issue.
"""
md = MarkdownIt("commonmark", {"html": False}).use(
tasklists_plugin, label=True, label_after=True
)
labels = re.findall(r"<label [^>]*>(.*?)</label>", md.render("- [ ] a &amp; b\n"))
assert labels == [" a &amp;amp; b"]
Loading