You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The DISCORD_ALLOWED_ROLES allowlist introduced in #11608 (authored by @0xyg3n in #9873) is not guild-scoped. Role membership in any mutual guild authorizes the user globally, including in DMs where no originating guild context exists.
Exploit path
Operator runs Hermes with DISCORD_ALLOWED_ROLES=<role_id> intending to authorize moderators of a private trusted server B.
The bot is also in a large public server A (community server, support server, etc.).
An attacker obtains <role_id> in server A (role-ID collisions across servers are easy to engineer; many public servers hand out colorful non-privileged roles on reaction or join).
Attacker DMs the bot. The allowlist iterates self._client.guilds, finds the role in server A, and authorizes the DM.
Attacker now has authenticated access to the bot: tool calls, memory reads, LLM calls billed to the operator, file access, any cross-service side effect the bot exposes.
The same flaw permits authorized chat in guild A's channels when the role was configured for guild B, by iterating mutual guilds instead of checking message.guild.
Why this is critical
No operator is safe. Any Hermes deployment with DISCORD_ALLOWED_ROLES configured is exposed if the bot joins any public server.
Scope-changed (S:C): privilege obtained in one guild applies to a fundamentally different guild/DM — classic authorization boundary break.
No mitigation short of disabling DISCORD_ALLOWED_ROLES until fixed. Operators who don't know about the flaw have no signal.
Code location
gateway/platforms/discord.py, function _is_allowed_user:
Signature takes no guild / message argument, so callers can't provide origin context.
DM path (guild=None) silently falls back to the cross-guild scan.
Fix
PR #12135 scopes role checks to the originating guild and disables role-based DM auth by default, with an explicit opt-in (DISCORD_DM_ROLE_AUTH_GUILD=<guild_id>) for operators who want it for a single trusted guild.
9 regression tests covering the bypass, the opt-in, the cross-guild guild-message bypass, and backwards-compat user-ID paths.
Security issue
Severity: CVSS 8.1 (High) — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N
The
DISCORD_ALLOWED_ROLESallowlist introduced in #11608 (authored by @0xyg3n in #9873) is not guild-scoped. Role membership in any mutual guild authorizes the user globally, including in DMs where no originating guild context exists.Exploit path
DISCORD_ALLOWED_ROLES=<role_id>intending to authorize moderators of a private trusted server B.<role_id>in server A (role-ID collisions across servers are easy to engineer; many public servers hand out colorful non-privileged roles on reaction or join).self._client.guilds, finds the role in server A, and authorizes the DM.The same flaw permits authorized chat in guild A's channels when the role was configured for guild B, by iterating mutual guilds instead of checking
message.guild.Why this is critical
DISCORD_ALLOWED_ROLESconfigured is exposed if the bot joins any public server.DISCORD_ALLOWED_ROLESuntil fixed. Operators who don't know about the flaw have no signal.Code location
gateway/platforms/discord.py, function_is_allowed_user:Two flaws:
guild/messageargument, so callers can't provide origin context.Fix
PR #12135 scopes role checks to the originating guild and disables role-based DM auth by default, with an explicit opt-in (
DISCORD_DM_ROLE_AUTH_GUILD=<guild_id>) for operators who want it for a single trusted guild.Recommendation
DISCORD_ALLOWED_ROLESset in production is currently exposed.DISCORD_ALLOWED_ROLESbehavior tightening and the newDISCORD_DM_ROLE_AUTH_GUILDopt-in.References
cc @teknium1