Описание
Federation upstream in RabbitMQ skips vhost authorization allowing cross-vhost message access
Summary
- What the bug lets you do. A
policymakeron one vhost reads and drains messages out of another vhost it has no permission on. With the defaultack-modethe source messages are consumed (deleted), not copied. - Why that should not work. vhost isolation is RabbitMQ's tenancy boundary.
policymakeris 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_accesson 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 tagv4.3.2(a509158b). The vulnerable code is unchanged onmainand 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.
mallorytries to reach/victimdirectly and is refused, over both the management API and AMQP:
- Step 2 — the working request. As
policymakeron/attacker,mallorydeclares 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):
- Step 3 — the payoff.
malloryattaches an AMQP consumer to her/attacker/secretsqueue. Federation opensamqp:///%2Fvictimas an internal direct connection authenticated asuser=none, the dummy backend approves every check, and/victim's messages are pulled into/attacker:
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
- Federation validates the upstream URI without any vhost-access check.
rabbit_federation_parameters.erl:50-54— thefederation-upstreamclause ofvalidate/5discards the creating user (_User).:98-115—validate_uri/2only callsamqp_uri:parse/1; nocheck_vhost_access.
- Only a per-vhost
policymakertag is required, checked against the request-path vhost — never the vhost inside the URI value.rabbit_mgmt_wm_parameter.erl:84-85— PUT gates onis_authorized_policies/2.rabbit_web_dispatch_access_control.erl:201-209—is_authorized_policies=is_admin orelse (is_policymaker andalso user_matches_vhost(...));user_matches_vhost/2checks only the vhost segment of the request path (vhost A), not the target vhost embedded inuri.
- The upstream connection params are built straight from the URI, with no user injected.
rabbit_federation_upstream.erl:69-71—to_params/2callsamqp_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{}defaultsusername = none.
- The direct connection therefore authenticates as the dummy (no-auth) user, whose access checks are hardcoded true.
rabbit_direct.erl:51-52—auth_fun({none,_},…)returnsrabbit_auth_backend_dummy:user().:187—connect1/4still callscheck_vhost_access(User, VHost,…), butUseris 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-37—check_vhost_access/check_resource_access/check_topic_accessall hardcoded-> true.- Confirmed live:
GET /api/connectionsduring the attack shows the federation link's connection into/victimas"user":"none","protocol":"Direct 0-9-1".
- The federation link then runs against the attacker-controlled direct params.
rabbit_federation_link_util.erl:35-52—start_conn_ch/5opens the upstream withopen_monitor(UParams#upstream_params.params, …)at:52.- Default
ack-modeison-confirm(rabbit_federation_upstream.erl:138), so forwarded messages are removed from the source queue — the read is destructive.
Impact
- Cross-vhost message read/drain from a per-vhost
policymaker, breaking vhost tenancy isolation. - Requires only the
policymakertag 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.
-
Start the broker;
rabbitmq-plugins enable rabbitmq_federation rabbitmq_federation_management. Create vhosts/attackerand/victim. -
Create user
mallory(tagpolicymakeronly); grant full perms on/attackerand none on/victim. -
As an operator, in
/victimdeclare a durable queuesecretsand publishVICTIM_SECRET_1/2/3(e.g.PUT /api/queues/%2Fvictim/secrets {"durable":true}thenPOST .../publish). Depth is now 3. -
As
mallory, confirm/victimis refused directly:
- As
mallory, three calls on/attacker, each returns HTTP 201:
- 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:
Пакеты
rabbitmq
>= 4.3.0, < 4.3.3
4.3.3
rabbitmq
>= 4.2.0, < 4.2.9
4.2.9
rabbitmq
>= 4.1.0, < 4.1.14
4.1.14
rabbitmq
>= 4.0.0, < 4.0.24
4.0.24
rabbitmq
>= 3.13.0, < 3.13.18
3.13.18