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: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ See the [Contributing Guide](contributing.md) for details.

### Fixed

* Sanitize attribute names in the `fenced_code` and `legacy_attrs` extensions so an
attribute name cannot break out of its tag and inject markup (#1663).
* For backwards compatibility in some languages, revert the enforcement of CommonMark
style punctuation rules (#1657).

Expand Down
23 changes: 17 additions & 6 deletions markdown/extensions/attr_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,28 @@ def isheader(elem: Element) -> bool:
return elem.tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']


NAME_RE = re.compile(r'[^A-Z_a-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02ff'
r'\u0370-\u037d\u037f-\u1fff\u200c-\u200d'
r'\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff'
r'\uf900-\ufdcf\ufdf0-\ufffd'
r'\:\-\.0-9\u00b7\u0300-\u036f\u203f-\u2040]+')


def sanitize_name(name: str) -> str:
"""
Sanitize name as 'an XML Name, minus the `:`.'
See <https://www.w3.org/TR/REC-xml-names/#NT-NCName>.
"""
return NAME_RE.sub('_', name)


class AttrListTreeprocessor(Treeprocessor):

BASE_RE = r'\{\:?[ ]*([^\}\n ][^\n]*)[ ]*\}'
HEADER_RE = re.compile(r'[ ]+{}[ ]*$'.format(BASE_RE))
BLOCK_RE = re.compile(r'\n[ ]*{}[ ]*$'.format(BASE_RE))
INLINE_RE = re.compile(r'^{}'.format(BASE_RE))
NAME_RE = re.compile(r'[^A-Z_a-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02ff'
r'\u0370-\u037d\u037f-\u1fff\u200c-\u200d'
r'\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff'
r'\uf900-\ufdcf\ufdf0-\ufffd'
r'\:\-\.0-9\u00b7\u0300-\u036f\u203f-\u2040]+')
NAME_RE = NAME_RE

def run(self, doc: Element) -> None:
for elem in doc.iter():
Expand Down Expand Up @@ -190,7 +201,7 @@ def sanitize_name(self, name: str) -> str:
Sanitize name as 'an XML Name, minus the `:`.'
See <https://www.w3.org/TR/REC-xml-names/#NT-NCName>.
"""
return self.NAME_RE.sub('_', name)
return sanitize_name(name)


class AttrListExtension(Extension):
Expand Down
5 changes: 3 additions & 2 deletions markdown/extensions/fenced_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from . import Extension
from ..preprocessors import Preprocessor
from .codehilite import CodeHilite, CodeHiliteExtension, parse_hl_lines
from .attr_list import get_attrs_and_remainder, AttrListExtension
from .attr_list import get_attrs_and_remainder, sanitize_name, AttrListExtension
from ..util import parseBoolValue
from ..serializers import _escape_attrib_html
import re
Expand Down Expand Up @@ -157,7 +157,8 @@ def run(self, lines: list[str]) -> list[str]:
# pairs were defined on the code block, and the `use_pygments` key was not set to `True`. The
# `use_pygments` key could be either set to `False` or not defined. It is omitted from output.
kv_pairs = ''.join(
f' {k}="{_escape_attrib_html(v)}"' for k, v in config.items() if k != 'use_pygments'
f' {sanitize_name(k)}="{_escape_attrib_html(v)}"'
for k, v in config.items() if k != 'use_pygments'
)
code = self._escape(m.group('code'))
code = f'<pre{id_attr}{class_attr}><code{lang_attr}{kv_pairs}>{code}</code></pre>'
Expand Down
3 changes: 2 additions & 1 deletion markdown/extensions/legacy_attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import re
from markdown.treeprocessors import Treeprocessor, isString
from markdown.extensions import Extension
from markdown.extensions.attr_list import sanitize_name
from typing import TYPE_CHECKING

if TYPE_CHECKING: # pragma: no cover
Expand All @@ -57,7 +58,7 @@ def run(self, doc: etree.Element) -> None:
def handleAttributes(self, el: etree.Element, txt: str) -> str:
""" Set attributes and return text without definitions. """
def attributeCallback(match: re.Match[str]):
el.set(match.group(1), match.group(2).replace('\n', ' '))
el.set(sanitize_name(match.group(1)), match.group(2).replace('\n', ' '))
return ATTR_RE.sub(attributeCallback, txt)


Expand Down
19 changes: 19 additions & 0 deletions tests/test_syntax/extensions/test_fenced_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,25 @@ def testFencedLanguageNoCodehiliteWithAttrList(self):
extensions=['fenced_code', 'attr_list']
)

def testFencedCodeAttrNameSanitizedNoCodehiliteWithAttrList(self):
# An attribute name is sanitized so it cannot break out of the `code` tag (see #1663).
self.assertMarkdownRenders(
self.dedent(
'''
``` { .python x><svg/onload=alert(1) }
# Some python code
```
'''
),
self.dedent(
'''
<pre><code class="language-python" x_svg_onload="alert(1)"># Some python code
</code></pre>
'''
),
extensions=['fenced_code', 'attr_list']
)

def testFencedLanguagePygmentsDisabledInAttrNoCodehiliteWithAttrList(self):
self.assertMarkdownRenders(
self.dedent(
Expand Down
8 changes: 8 additions & 0 deletions tests/test_syntax/extensions/test_legacy_attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,11 @@ def testLegacyAttrs(self):
"""), # noqa: E501
extensions=['legacy_attrs']
)

def testLegacyAttrsSanitizesName(self):
# An attribute name is sanitized so it cannot break out of the element (see #1663).
self.assertMarkdownRenders(
'text {@x><svg/onload=alert(1)=y} more',
'<p x_svg_onload_alert_1_="y">text more</p>',
extensions=['legacy_attrs']
)
Loading