fix(peer): route hermes peer dm through the credential-redirect guard - #88819
pierrenode wants to merge 1 commit into
Conversation
hermes_cli/subcommands/peer.py's _request() sends the peer's Authorization: Bearer <API_SERVER_KEY> via a raw urllib.request.urlopen() call. Python's default HTTPRedirectHandler preserves all request headers across a 3xx redirect, including Authorization, even when the redirect crosses origins. A compromised peer gateway (or a LAN MITM answering the URL registered with `hermes peer add`) can redirect to an attacker- controlled host and harvest the peer key -- which bots also send autonomously via `hermes peer dm`, per the Bot Mode messaging protocol injected by tools/bot_mode_probe.py. This is the same credential-redirect-leak class this repo has closed repeatedly elsewhere (providers/base.py, hermes_cli/models.py, azure_detect.py, the anthropic adapter, plugins/model-providers/actual) via hermes_cli/urllib_security.py::open_credentialed_url(), which strips non-safelisted headers whenever a redirect crosses origin. peer.py never adopted it. Route _request() through open_credentialed_url() -- a drop-in swap for urlopen() since it accepts the same pre-built Request object. Added a regression test using two real loopback HTTP servers (the peer and a stand-in attacker origin): a peer that 302-redirects every request must not leak the Bearer key to the redirect target. Mutation-verified: reverting the fix makes the new test fail with the key present at the attacker origin.
Correct and well-proven: — reviewer-b (automated review) No blocking issues found. |
|
@pierrenode I am composing this credential-redirect fix into draft #95965 because RoomLink depends on the same peer HTTP boundary. Your commit and authorship are preserved unchanged. If this lands first, I will drop the duplicate patch from the composition. |
|
Thanks @pierrenode — this landed on main in e743391: |
What
hermes_cli/subcommands/peer.py's_request()(the shared HTTP helper forhermes peer add/list/dm) sends the peer'sAuthorization: Bearer <API_SERVER_KEY>via a rawurllib.request.urlopen()call. Python's defaultHTTPRedirectHandlerpreserves all request headers — includingAuthorization— across a 3xx redirect, even when the redirect target is a different origin than the URL registered withhermes peer add.Why it matters
A compromised peer gateway, or a LAN MITM answering the peer's registered URL, can respond with a redirect to an attacker-controlled host and harvest the peer's API key. This isn't a theoretical/manual-only path either: bots on a Bot-Mode-managed install are told (via
tools/bot_mode_probe.py's injected "Messaging other agents" protocol section) to usehermes peer dm <peer>/<agent> "..."autonomously to message teammates on other machines, so the leak can be triggered without a human in the loop.This is the exact credential-redirect-leak class this repo has closed repeatedly in other outbound HTTP call sites (
providers/base.py,hermes_cli/models.py,hermes_cli/azure_detect.py, the Anthropic adapter,plugins/model-providers/actual) viahermes_cli/urllib_security.py::open_credentialed_url(), which strips non-safelisted headers whenever a redirect crosses origin.peer.py— added in this same window as the newhermes peer/Bot Mode cross-machine messaging feature — never adopted it.Fix
Route
_request()throughopen_credentialed_url()instead ofurlopen()directly. It's a drop-in swap: both accept the same pre-builtRequestobject andtimeoutkwarg; only the redirect-handling opener changes.Testing
test_request_strips_bearer_key_across_redirect_origintotests/hermes_cli/test_peer_cmd.py, using two real loopbackHTTPServerinstances (the "peer," which 302-redirects every request, and a stand-in attacker origin) to prove theAuthorizationheader does not reach the redirect target.peer's Bearer key leaked to the redirect target: ['Bearer top-secret-peer-key']).tests/hermes_cli/test_peer_cmd.py(11 tests) andtests/hermes_cli/test_urllib_security.py(16 tests) pass together.ruff checkclean on both changed files.Checklist