Summary
filters.command() interpolates the user-supplied command name directly into two regular expressions without re.escape(). Any command containing a regex metacharacter (. + ? ( [ etc.) either silently matches messages it must not match, or raises re.error during update dispatch.
The same applies to the follow-up re.sub() that strips the command from the text to build message.command, so arguments are also extracted incorrectly for such commands.
Note: this is a static analysis finding based on reading the dev branch; no runtime execution was performed.
Location
- File:
pyrogram/filters.py
- Function:
command() (inner func)
- Relevant code path:
for cmd in flt.commands:
if not re.match(rf"^(?:{cmd}(?:@?{username})?)(?:\s|$)", without_prefix,
flags=re.IGNORECASE if not flt.case_sensitive else 0):
continue
without_command = re.sub(rf"{cmd}(?:@?{username})?\s?", "", without_prefix, count=1, ...)
Problem
The documented parameter is a plain command name (commands (str | list): The command or list of commands as string), but the string is embedded in a pattern as raw regex source:
- Metacharacters alter matching semantics. For
filters.command("buy.now"), the compiled intent is literal buy.now, but the executed pattern ^(?:buy.now(?:@?mybot)?)(?:\s|$) matches /buyXnow, /buy-now, /buynow2… because . matches any character.
- Quantifier-bearing commands produce broader false matches. For
"c++", the effective pattern means "one-or-more of one-or-more c", so /cc and /cccc trigger the c++ handler.
- Some inputs make the pattern invalid. For
"what?", compiling ^(?:what?(?:@?user)?)(?:\s|$) succeeds (t? quantifies the t) and then matches /wha, /what, /whatt...; for a command ending in an orphan quantifier such as "+" or "*" (e.g. command("+")), re.match raises re.error: nothing to repeat at position ... for every incoming message that starts with the configured prefix, turning the filter into a dispatch-time crash.
Because the second re.sub() reuses the same raw interpolation, even the false-positive cases strip the wrong span from without_prefix, producing wrong message.command argument lists.
Trigger / Reproduction
@app.on_message(filters.command("buy.now"))
async def handler(client, message):
...
Sending any of /buyXnow, /buy-now (with any configured prefix) invokes this handler although the user typed a different command.
@app.on_message(filters.command("+"))
Every incoming message beginning with / makes func raise re.error inside the dispatcher instead of returning a boolean.
Expected Behavior
Command names should be matched literally regardless of their characters, e.g. by escaping before interpolation:
escaped = re.escape(cmd)
re.match(rf"^(?:{escaped}(?:@?{re.escape(username)})?)(?:\s|$)", ...)
Actual Behavior
Command names act as regex patterns; metacharacter-containing commands misfire or raise re.error.
Impact
- Silent routing bugs for legitimate command naming styles (
start.v2, buy-now, localized prefixes like !?, technical commands like c++).
- A single bad command can spam exceptions on every prefixed message, flooding logs and preventing the affected handlers from ever running deterministically.
- Wrong
message.command argument extraction follows from the same root cause.
Suggested Direction
Apply re.escape() to cmd (and defensively to username) at the point of interpolation into both the re.match and re.sub patterns. Optionally precompile per-command patterns once at filter creation instead of per-message.
Evidence
pyrogram/filters.py, command() inner function on the current dev branch: both f-string interpolations shown above contain no re.escape.
- Docstring contract ("The command or list of commands as string") implies literal matching, not pattern semantics.
Summary
filters.command()interpolates the user-supplied command name directly into two regular expressions withoutre.escape(). Any command containing a regex metacharacter (.+?([etc.) either silently matches messages it must not match, or raisesre.errorduring update dispatch.The same applies to the follow-up
re.sub()that strips the command from the text to buildmessage.command, so arguments are also extracted incorrectly for such commands.Note: this is a static analysis finding based on reading the
devbranch; no runtime execution was performed.Location
pyrogram/filters.pycommand()(innerfunc)Problem
The documented parameter is a plain command name (
commands (str | list): The command or list of commands as string), but the string is embedded in a pattern as raw regex source:filters.command("buy.now"), the compiled intent is literalbuy.now, but the executed pattern^(?:buy.now(?:@?mybot)?)(?:\s|$)matches/buyXnow,/buy-now,/buynow2… because.matches any character."c++", the effective pattern means "one-or-more of one-or-morec", so/ccand/cccctrigger thec++handler."what?", compiling^(?:what?(?:@?user)?)(?:\s|$)succeeds (t?quantifies thet) and then matches/wha,/what,/whatt...; for a command ending in an orphan quantifier such as"+"or"*"(e.g.command("+")),re.matchraisesre.error: nothing to repeat at position ...for every incoming message that starts with the configured prefix, turning the filter into a dispatch-time crash.Because the second
re.sub()reuses the same raw interpolation, even the false-positive cases strip the wrong span fromwithout_prefix, producing wrongmessage.commandargument lists.Trigger / Reproduction
Sending any of
/buyXnow,/buy-now(with any configured prefix) invokes this handler although the user typed a different command.@app.on_message(filters.command("+"))Every incoming message beginning with
/makesfuncraisere.errorinside the dispatcher instead of returning a boolean.Expected Behavior
Command names should be matched literally regardless of their characters, e.g. by escaping before interpolation:
Actual Behavior
Command names act as regex patterns; metacharacter-containing commands misfire or raise
re.error.Impact
start.v2,buy-now, localized prefixes like!?, technical commands likec++).message.commandargument extraction follows from the same root cause.Suggested Direction
Apply
re.escape()tocmd(and defensively tousername) at the point of interpolation into both there.matchandre.subpatterns. Optionally precompile per-command patterns once at filter creation instead of per-message.Evidence
pyrogram/filters.py,command()inner function on the currentdevbranch: both f-string interpolations shown above contain nore.escape.