Skip to content
Open
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
93 changes: 83 additions & 10 deletions mdit_py_plugins/attrs/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from collections.abc import Sequence
from functools import partial
import re
from typing import Any

from markdown_it import MarkdownIt
Expand Down Expand Up @@ -56,10 +57,24 @@ def attrs_plugin(
:param allowed: A list of allowed attribute names.
If not ``None``, any attributes not in this list will be removed
and placed in the token's meta under the key "insecure_attrs".
If ``None`` (the default), event-handler attributes
(any name starting with ``on``, such as ``onclick``) are still removed,
as a baseline protection against script injection.
Other attributes, including ``style``, are kept.
Whatever ``allowed`` says, the URL-valued attributes
``href``, ``src``, ``action``, ``formaction`` and ``xlink:href``
are removed when ``md.validateLink`` rejects their value
(for example a ``javascript:`` URL), as for Markdown links and images.
This is not a full sanitiser: to safely render untrusted input,
pass an explicit ``allowed`` list (e.g. ``("id", "class")``).
"""

if spans:
md.inline.ruler.after(span_after, "span", _span_rule)
md.inline.ruler.after(
span_after,
"span",
partial(_span_rule, allowed=None if allowed is None else set(allowed)),
)
if after:
md.inline.ruler.push(
"attr",
Expand Down Expand Up @@ -91,6 +106,16 @@ def attrs_block_plugin(md: MarkdownIt, *, allowed: Sequence[str] | None = None)
:param allowed: A list of allowed attribute names.
If not ``None``, any attributes not in this list will be removed
and placed in the token's meta under the key "insecure_attrs".
If ``None`` (the default), event-handler attributes
(any name starting with ``on``, such as ``onclick``) are still removed,
as a baseline protection against script injection.
Other attributes, including ``style``, are kept.
Whatever ``allowed`` says, the URL-valued attributes
``href``, ``src``, ``action``, ``formaction`` and ``xlink:href``
are removed when ``md.validateLink`` rejects their value
(for example a ``javascript:`` URL), as for Markdown links and images.
This is not a full sanitiser: to safely render untrusted input,
pass an explicit ``allowed`` list (e.g. ``("id", "class")``).
"""
md.block.ruler.before("fence", "attr", _attr_block_rule)
md.core.ruler.after(
Expand All @@ -115,7 +140,9 @@ def _find_opening(tokens: Sequence[Token], index: int) -> int | None:
return None


def _span_rule(state: StateInline, silent: bool) -> bool:
def _span_rule(
state: StateInline, silent: bool, *, allowed: set[str] | None = None
) -> bool:
if state.src[state.pos] != "[":
return False

Expand Down Expand Up @@ -144,7 +171,7 @@ def _span_rule(state: StateInline, silent: bool) -> bool:
state.pos = labelStart
state.posMax = labelEnd
token = state.push("span_open", "span", 1)
token.attrs = attrs # type: ignore[assignment]
_add_attrs(state.md, token, attrs, allowed)
state.md.inline.tokenize(state)
token = state.push("span_close", "span", -1)

Expand Down Expand Up @@ -177,7 +204,7 @@ def _attr_inline_rule(
attr_token = state.tokens[token_index]
if "class" in attrs and "class" in token.attrs:
attrs["class"] = f"{token.attrs['class']} {attrs['class']}"
_add_attrs(attr_token, attrs, allowed)
_add_attrs(state.md, attr_token, attrs, allowed)
return True


Expand Down Expand Up @@ -253,23 +280,69 @@ def _attr_resolve_block_rule(state: StateCore, *, allowed: set[str] | None) -> N
if key == "class" or key not in next_token.attrs:
next_token.attrs[key] = value
else:
_add_attrs(next_token, state.tokens[i].attrs, allowed)
_add_attrs(state.md, next_token, state.tokens[i].attrs, allowed)

state.tokens.pop(i)
len_tokens -= 1


_URL_ATTRS = frozenset(("href", "src", "action", "formaction", "xlink:href"))
"""Attributes whose value is a URL, checked with ``md.validateLink``."""

_URL_IGNORED_RE = re.compile(r"[\t\n\r]")
_URL_STRIP = "".join(chr(i) for i in range(0x21))


def _is_insecure_attr(key: str) -> bool:
"""Return True for event-handler attributes (``on*``, e.g. ``onclick``).

They execute JavaScript, so they are removed by default
when no explicit ``allowed`` list is given.
"""
return key.lower().startswith("on")


def _is_valid_url(md: MarkdownIt, value: Any) -> bool:
"""Return True if ``md.validateLink`` accepts a URL-valued attribute.

Browsers ignore ASCII tabs and newlines anywhere in a URL,
and control characters and spaces around it, before reading its scheme
(so ``java<TAB>script:`` is a ``javascript:`` URL).
The value is emitted as written, so those characters are removed
before it is normalised and validated.
"""
url = _URL_IGNORED_RE.sub("", str(value)).strip(_URL_STRIP)
return md.validateLink(md.normalizeLink(url))


def _add_attrs(
md: MarkdownIt,
token: Token,
attrs: dict[str, Any],
allowed: set[str] | None,
) -> None:
"""Add attributes to a token, skipping any disallowed attributes."""
if allowed is not None and (
disallowed := {k: v for k, v in attrs.items() if k not in allowed}
):
"""Add attributes to a token, skipping any disallowed attributes.

When ``allowed`` is not ``None`` only those names are kept.
When it is ``None`` (the default) event-handler (``on*``) attributes
are still removed, so untrusted input cannot inject script.
In both cases, a URL-valued attribute (``href``, ``src``, ...)
is removed if ``md.validateLink`` rejects it.
Anything removed is preserved in ``token.meta["insecure_attrs"]``.
"""
if allowed is not None:
disallowed = {k: v for k, v in attrs.items() if k not in allowed}
else:
disallowed = {k: v for k, v in attrs.items() if _is_insecure_attr(k)}
disallowed.update(
(k, v)
for k, v in attrs.items()
if k not in disallowed and k.lower() in _URL_ATTRS and not _is_valid_url(md, v)
)

if disallowed:
token.meta["insecure_attrs"] = disallowed
attrs = {k: v for k, v in attrs.items() if k in allowed}
attrs = {k: v for k, v in attrs.items() if k not in disallowed}

# attributes takes precedence over existing attributes
token.attrs.update(attrs)
4 changes: 2 additions & 2 deletions mdit_py_plugins/dollarmath/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def dollarmath_plugin(
if label_renderer is None:
_label_renderer = ( # noqa: E731
lambda label: (
f'<a href="#{label}" class="mathlabel" title="Permalink to this equation">¶</a>'
f'<a href="#{escapeHtml(label)}" class="mathlabel" title="Permalink to this equation">¶</a>'
)
)
else:
Expand Down Expand Up @@ -142,7 +142,7 @@ def render_math_block_label(
env: EnvType,
) -> str:
content = _renderer(str(tokens[idx].content).strip(), {"display_mode": True})
_id = tokens[idx].info
_id = escapeHtml(tokens[idx].info)
label = _label_renderer(tokens[idx].info)
return f'<div id="{_id}" class="math block">\n{label}\n{content}\n</div>\n'

Expand Down
2 changes: 1 addition & 1 deletion mdit_py_plugins/myst_blocks/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def render_myst_target(
options: OptionsDict,
env: EnvType,
) -> str:
label = tokens[idx].content
label = escapeHtml(tokens[idx].content)
class_name = "myst-target"
target = f'<a href="#{label}">({label})=</a>'
return f'<div class="{class_name}">{target}</div>'
Expand Down
7 changes: 4 additions & 3 deletions mdit_py_plugins/texmath/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from typing import TYPE_CHECKING, Any, TypedDict

from markdown_it import MarkdownIt
from markdown_it.common.utils import charCodeAt
from markdown_it.common.utils import charCodeAt, escapeHtml

if TYPE_CHECKING:
from markdown_it.renderer import RendererProtocol
Expand Down Expand Up @@ -65,7 +65,8 @@ def render_math_block(
env: EnvType,
) -> str:
return rule_block["tmpl"].format( # noqa: B023
render(tokens[idx].content, True, macros), tokens[idx].info
render(tokens[idx].content, True, macros),
escapeHtml(tokens[idx].info),
)

md.add_render_rule(rule_block["name"], render_math_block)
Expand Down Expand Up @@ -169,7 +170,7 @@ def dollar_post(src: str, end: int) -> bool:


def render(tex: str, displayMode: bool, macros: Any) -> str:
return tex
return escapeHtml(tex)
# TODO better HTML renderer port for math
# try:
# res = katex.renderToString(tex,{throwOnError:False,displayMode,macros})
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/myst_block.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Target characters:
.
(a bc |@<>*./_-+:)=
.
<div class="myst-target"><a href="#a bc |@<>*./_-+:">(a bc |@<>*./_-+:)=</a></div>
<div class="myst-target"><a href="#a bc |@&lt;&gt;*./_-+:">(a bc |@&lt;&gt;*./_-+:)=</a></div>
.

Empty target:
Expand Down
8 changes: 4 additions & 4 deletions tests/fixtures/texmath_bracket.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ simple equation including special html character. (valid=True)
.
\(1+1<3\)
.
<p><eq>1+1<3</eq></p>
<p><eq>1+1&lt;3</eq></p>
.

equation including backslashes. (valid=True)
Expand Down Expand Up @@ -239,8 +239,8 @@ multiline equation. (valid=True)
.
<section>
<eqn>\\begin{matrix}
f & = & 2 + x + 3 \\
& = & 5 + x
f &amp; = &amp; 2 + x + 3 \\
&amp; = &amp; 5 + x
\\end{matrix}</eqn>
</section>
.
Expand All @@ -253,7 +253,7 @@ vector equation. (valid=True)
.
<section>
<eqn>\\begin{pmatrix}x_2 \\\\ y_2 \\end{pmatrix} =
\\begin{pmatrix} A & B \\\\ C & D \\end{pmatrix}\\cdot
\\begin{pmatrix} A &amp; B \\\\ C &amp; D \\end{pmatrix}\\cdot
\\begin{pmatrix} x_1 \\\\ y_1 \\end{pmatrix}</eqn>
</section>
.
Expand Down
8 changes: 4 additions & 4 deletions tests/fixtures/texmath_dollar.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ simple equation including special html character. (valid=True)
.
$1+1<3$
.
<p><eq>1+1<3</eq></p>
<p><eq>1+1&lt;3</eq></p>
.

equation including backslashes. (valid=True)
Expand Down Expand Up @@ -239,8 +239,8 @@ $$\\begin{matrix}
.
<section>
<eqn>\\begin{matrix}
f & = & 2 + x + 3 \\
& = & 5 + x
f &amp; = &amp; 2 + x + 3 \\
&amp; = &amp; 5 + x
\\end{matrix}</eqn>
</section>
.
Expand All @@ -253,7 +253,7 @@ $$\\begin{pmatrix}x_2 \\\\ y_2 \\end{pmatrix} =
.
<section>
<eqn>\\begin{pmatrix}x_2 \\\\ y_2 \\end{pmatrix} =
\\begin{pmatrix} A & B \\\\ C & D \\end{pmatrix}\\cdot
\\begin{pmatrix} A &amp; B \\\\ C &amp; D \\end{pmatrix}\\cdot
\\begin{pmatrix} x_1 \\\\ y_1 \\end{pmatrix}</eqn>
</section>
.
Expand Down
Loading
Loading