Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save YLChen-007/f0036aa1c410b70f5e41272947180645 to your computer and use it in GitHub Desktop.

Select an option

Save YLChen-007/f0036aa1c410b70f5e41272947180645 to your computer and use it in GitHub Desktop.
hermes-agent: Hermes gateway quick commands bypass admin-only slash command restrictions

Advisory Details

Title: Hermes gateway quick commands bypass admin-only slash command restrictions

Description:

Summary

Hermes gateway enforces allow_admin_from and user_allowed_commands only for registered gateway slash commands. Operator-configured quick_commands are dispatched later on a separate path without the same authorization check, which allows an authenticated allowlisted non-admin messaging user to invoke admin-only quick commands, including type: exec commands that run in the gateway process.

Details

The vulnerable behavior is in gateway/run.py. During inbound slash-command handling, Hermes resolves the typed command and applies _check_slash_access(...) only when the canonical command is present in the gateway command registry:

if command and canonical and is_gateway_known_command(canonical):
    _denied = self._check_slash_access(source, canonical)
    if _denied is not None:
        return _denied

That gate is not applied to config-backed quick_commands. Later in the same handler, Hermes looks up the raw typed command name in quick_commands and executes type: exec commands directly:

if command in quick_commands:
    qcmd = quick_commands[command]
    if qcmd.get("type") == "exec":
        proc = await asyncio.create_subprocess_shell(...)

This creates a policy split. Registered slash commands such as /stop are denied for the same non-admin sender under the configured policy, but a non-registered quick command such as /limits still reaches the execution sink and returns command output.

I verified this with the real GatewayRunner._handle_message(...) entrypoint using real SessionSource and MessageEvent objects. The test policy was:

  • allow_admin_from: ["111"]
  • user_allowed_commands: []
  • quick_commands.limits = {type: exec, command: "printf quick-command-bypass-confirmed"}

Under that policy, a non-admin sender 999 received quick-command-bypass-confirmed from /limits, while the control input /stop returned the expected denial message:

⛔ /stop is admin-only here. No slash commands are enabled for non-admins on this platform.

PoC

Prerequisites

  • Hermes Agent checkout from the canonical upstream repository.
  • Python virtual environment available at ./.venv.
  • A configuration equivalent to:
    • allow_admin_from: ["111"]
    • user_allowed_commands: []
    • quick_commands.limits = {type: exec, command: "printf quick-command-bypass-confirmed"}
  • Attacker position: authenticated allowlisted non-admin gateway user in DM scope.

Reproduction Steps

  1. Download the verification PoC from: verification_test.py
  2. Download the control PoC from: control-non-admin-builtin-denied.py
  3. From the repository root, run the verification case: ./.venv/bin/python llm-enhance/cve-finding/similar/Auth-Privilege/GHSA-2hm8-rqrm-xfjq-quick-commands-slash-access-bypass-exp/verification_test.py
  4. Observe the verification output includes: quick-command-bypass-confirmed
  5. Run the control case under the same policy: ./.venv/bin/python llm-enhance/cve-finding/similar/Auth-Privilege/GHSA-2hm8-rqrm-xfjq-quick-commands-slash-access-bypass-exp/control-non-admin-builtin-denied.py
  6. Observe the control output includes an admin-only denial for /stop.

Log of Evidence

Verification log excerpt:

{
  "mode": "Integration-Test",
  "input": "/limits",
  "user_id": "999",
  "policy": {
    "allow_admin_from": ["111"],
    "user_allowed_commands": []
  },
  "observed_result": "quick-command-bypass-confirmed"
}

Control log excerpt:

{
  "mode": "Integration-Test",
  "input": "/stop",
  "user_id": "999",
  "observed_result": "⛔ /stop is admin-only here. No slash commands are enabled for non-admins on this platform. Ask an admin to add you to allow_admin_from or to set user_allowed_commands."
}

Impact

This is an authorization bypass on Hermes' messaging-gateway slash-command surface. Operators who rely on allow_admin_from and user_allowed_commands to separate admin-only and non-admin slash capabilities can unintentionally expose configured quick commands to non-admin gateway users. When the quick command uses type: exec, the non-admin user can trigger shell command execution in the gateway process context, subject to the operator-defined command body. That can expose sensitive operational data, modify system state, or disrupt service depending on what the quick command does.

Affected products

  • Ecosystem: pip
  • Package name: hermes-agent
  • Affected versions: <= 2026.6.5
  • Patched versions:

Severity

  • Severity: High
  • Vector string: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Weaknesses

  • CWE: CWE-863: Incorrect Authorization

Occurrences

Permalink Description
https://github.com/NousResearch/hermes-agent/blob/3c231eb3979ab9c57d5cd6d02f1d577a3b718b43/gateway/run.py#L8092-L8101 Hermes documents and enforces slash access control only for commands that resolve through is_gateway_known_command(canonical), leaving non-registered quick-command names outside the gate.
https://github.com/NousResearch/hermes-agent/blob/3c231eb3979ab9c57d5cd6d02f1d577a3b718b43/gateway/run.py#L8344-L8367 The quick_commands dispatch path executes type: exec commands with asyncio.create_subprocess_shell(...) and returns output without re-running _check_slash_access(...) for the typed slash command.
https://github.com/NousResearch/hermes-agent/blob/3c231eb3979ab9c57d5cd6d02f1d577a3b718b43/gateway/run.py#L10231-L10271 _check_slash_access(...) is the intended admin/non-admin policy primitive. Its denial message confirms that non-admin users with empty user_allowed_commands should not be able to invoke admin-only slash capabilities.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment