Skip to content

Sanitize attribute names in fenced_code and legacy_attrs - #1663

Merged
waylan merged 1 commit into
Python-Markdown:masterfrom
naaz234:sanitize-attr-names-xss
Oct 19, 2026
Merged

waylan merged 1 commit into
Python-Markdown:masterfrom
naaz234:sanitize-attr-names-xss

Conversation

@naaz234

@naaz234 naaz234 commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Description

fenced_code (the non-Pygments path, with attr_list enabled) and legacy_attrs write a user-supplied attribute name into the output without sanitizing it. In fenced_code the key of a key=value pair in a fence's {...} is emitted as f' {k}="..."' with only the value escaped. In legacy_attrs the name from {@name=value} is set straight onto the element with el.set(match.group(1), ...) and the serializer writes attribute names verbatim. The attr_list scanner allows a name of [^ =}]+ and legacy_attrs allows [^}]*, both of which permit >, <, / and quotes, so a name like x><svg/onload=alert(1) closes the <code> tag (or the surrounding element) and injects a live element.

attr_list already guards the element path with sanitize_name (NAME_RE.sub('_', name)), which is where a name like this belongs. This moves that helper to module scope and applies it at the two sites that skipped it, so a name is reduced to a valid XML name before it reaches the output. Valid names (letters, digits, -, ., _, :) are unchanged, so existing attribute lists render the same.

Reproduction before the change:

>>> import markdown
>>> markdown.markdown('``` { .lang x><svg/onload=alert(1) }\ncode\n```', extensions=['fenced_code', 'attr_list'])
'<pre><code class="language-lang" x><svg/onload="alert(1)">code\n</code></pre>'
>>> markdown.markdown('text {@x><svg/onload=alert(1)=y} more', extensions=['legacy_attrs'])
'<p x><svg/onload=alert(1)="y">text  more</p>'

In both cases the > in the name closes the enclosing tag and a live <svg onload=...> element follows. After the change the name becomes x_svg_onload, so it stays inside the intended tag.

Added regression tests for both extensions and a changelog entry.

AI Assistance Disclosure

  • No AI tools were used in preparing this PR.
  • If AI tools were used, I have disclosed which ones, and fully reviewed and verified their output.

Checklist

An attribute name supplied through the attr_list {attrs} syntax on a fenced
code block, or through the legacy_attrs {@name=value} syntax, was written to
the output without sanitization. The attr_list scanner permits a name of
[^ =}]+ and legacy_attrs permits [^}]*, both of which allow >, <, / and
quotes, so a name such as x><svg/onload=alert(1) closed the enclosing tag and
injected a live element. Reuse attr_list.sanitize_name, which the element path
already applies, at both sites. Valid names are unchanged.

@waylan waylan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. As this change only adds sanitation in the 2 places it was getting missed, this is an acceptable fix. Thanks for the work on this.

@waylan
waylan merged commit a27428b into Python-Markdown:master Oct 19, 2026
15 of 16 checks passed
@waylan waylan added the approved The pull request is ready to be merged. label Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved The pull request is ready to be merged.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants