Skip to content

Escape the language in hpre so it can't break the class attribute - #2644

Merged
Badiboy merged 1 commit into
eternnoir:masterfrom
beduldul:fix/hpre-language-escape
Oct 25, 2026
Merged

Badiboy merged 1 commit into
eternnoir:masterfrom
beduldul:fix/hpre-language-escape

Conversation

@beduldul

Copy link
Copy Markdown
Contributor

Description

hpre() escapes content but not language, which goes straight into the class attribute:

>>> hpre("x", language='a"b')
'<pre><code class="a"b">x</code></pre>'

Telegram rejects that as malformed HTML. A crafted language adds attributes instead of breaking the tag:

>>> hpre("x", language='a" onmouseover="alert(1)')
'<pre><code class="a" onmouseover="alert(1)">x</code></pre>'

Parsing that back gives ('code', [('class', 'a'), ('onmouseover', 'alert(1)')]).

The fix runs language through escape_html() the same way content already is. html.escape() only touches & < > " ', so ordinary values like python or c++ come out unchanged.

mcode() has a similar problem - a backtick in language closes the code fence - but that needs a different fix, so I left it out of this one.

Describe your tests

New tests/test_formatting.py; the module had no tests before.

Two cases fail on master and pass with the change: a quote in the language, and the attribute injection. Two more cover the plain-language path and confirm content is still escaped.

Without the change: 2 failed, 2 passed. With it: 4 passed. Full suite: 288 passed, 2 skipped.

Python version: 3.14.9

OS: macOS

Checklist:

  • I added/edited example on new feature/change (if exists)
  • My changes won't break backward compatibility
  • I made changes both for sync and async

hpre() inserts language straight into the class attribute while escaping
content. A quote in the language ends the attribute early:

    >>> hpre("x", language='a"b')
    '<pre><code class="a"b">x</code></pre>'

Telegram rejects that as malformed HTML, and a crafted language adds
attributes instead: hpre("x", language='a" onmouseover="alert(1)') parses
as class="a" onmouseover="alert(1)".

Run language through escape_html() the same way content already is. Plain
values are unchanged, since html.escape() only touches & < > " '.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The narrowly scoped fix addresses the attribute injection issue, with focused regression coverage and no identified blocking issues.

No open issues 🎉

What changed in this PR

This PR fixes HTML attribute escaping in hpre() so quotes or crafted language values cannot break the generated class attribute or inject additional attributes.

Changes:

  • Escape language with escape_html() while preserving existing content-escaping behavior.
  • Add four regression tests covering ordinary language values, quotes, attribute injection, and content escaping.
File Description
tests/​test_formatting.py Adds regression coverage for safe hpre() HTML output.
telebot/​formatting.py Escapes the language value before inserting it into the class attribute.

Review effort: Balanced

@Badiboy

Badiboy commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator

Do not see any reason for setting language like 'a"b' and passing "onmouseover" that is not supported by Telegram.

FORMALLY you are right, 'a"b' is parsed incorrectly, butdo not see when 'a"b' should be used as language.

@beduldul

Copy link
Copy Markdown
Contributor Author

Fair - a"b isn't a real language. The case where it matters is a bot that takes the language from the message instead of hardcoding it:

lang, _, code = message.text.partition("\n")
bot.reply_to(message, hpre(code, language=lang), parse_mode="HTML")

lang is whatever the user typed, and one quote in it makes Telegram reject the whole message rather than just the language. Real values like python or c++ come out unchanged, so it's a one-line change with no behaviour difference otherwise - but if you'd still rather not, say so and I'll close it.

@Badiboy

Badiboy commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator

👌

@Badiboy
Badiboy merged commit fd1fbb2 into eternnoir:master Oct 25, 2026
7 checks passed
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.

3 participants