Skip to content

Escape language name in codehilite non-Pygments output - #1667

Merged
waylan merged 2 commits into
Python-Markdown:masterfrom
naaz234:codehilite-escape-lang-class
Oct 21, 2026
Merged

waylan merged 2 commits into
Python-Markdown:masterfrom
naaz234:codehilite-escape-lang-class

Conversation

@naaz234

@naaz234 naaz234 commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Description

The codehilite extension's non-Pygments output builds the <code> element's class attribute from the language name without escaping it. In CodeHilite.hilite, the fallback branch (reached when Pygments is not installed or use_pygments is False) does classes.append('{}{}'.format(self.lang_prefix, self.lang)) and then ' class="{}"'.

When codehilite is used together with attr_list, the language is taken from a fenced-block class token. The attr_list scanner allows any character in a class token except space, = and }, which includes ", < and >. So an input such as:

```{.a"><script>alert(1)</script>}
code
```

produces class="language-a"><script>alert(1)</script>", which closes the attribute and injects a live <script> element (stored XSS for anything rendering untrusted Markdown in that configuration). The language can also be set directly, e.g. CodeHilite('x', lang='"><script>', use_pygments=False).

The fenced_code extension already escapes the language in its own non-Pygments branch with _escape_attrib_html; this applies the same escaping at the one site in codehilite that missed it. Valid language names are unchanged. Added a regression test (runs without Pygments via use_pygments=False) and a changelog entry.

AI Assistance Disclosure

  • No AI tools were used in preparing this PR.

  • AI tools were used and I have disclosed which ones, and have fully reviewed and verified their output.

    AI tools were use to assist with the following:

    • Code
    • Code Comments
    • Documentation
    • Changelog Entry
    • Commit Message
    • The Description Above
    • Other: ______________

    Claude (Anthropic) was used to help locate the issue, draft the patch, the test, the changelog entry, the commit message, and this description. I reviewed and verified all of it, and can explain the change.

Checklist

naaz234 and others added 2 commits October 20, 2026 22:32
When Pygments is unavailable (or use_pygments is False), CodeHilite.hilite built the code element's class attribute from the language name without escaping it. With attr_list enabled the language comes from a fenced-block class token, whose scanner permits ", < and >, so a value could close the class attribute and inject markup. fenced_code already escapes the language in its own non-Pygments branch; apply the same escaping in codehilite's fallback.
@waylan

waylan commented Oct 21, 2026

Copy link
Copy Markdown
Member

I want to point out our policy on Sanitizing Output. In short, that is out-of-scope. That said, if we are normally escaping an input and are only missing it in one edge case, that is different. In the future, please do not present these sorts of fixes as preventing XSS. I have edited the changelog entry to better reflect the justification for the change.

@waylan
waylan merged commit 3d70cfc into Python-Markdown:master Oct 21, 2026
14 checks passed
@naaz234

naaz234 commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Understood, I hadn't read the sanitisation policy closely enough. I'll frame these as consistency fixes rather than XSS going forward, and the changelog wording you went with reads better. Thanks for merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants