What Four RabbitMQ Advisories Reveal About Broker Security Boundaries

TL;DR: Cantina’s Appsec Agent found 4 RabbitMQ security issues. They affected OAuth permission changes, topic permissions, loopback-only users, and Direct Reply-To bindings.
RabbitMQ is easy to think of as infrastructure plumbing. Applications publish messages, consumers receive them, and the broker handles everything in between.
But that “in between” includes authenticating clients, enforcing permissions, resolving routing decisions, and maintaining state across long-lived connections. When one of those decisions is wrong, the impact can extend beyond the broker and break the isolation assumptions made by the applications using it.
RabbitMQ is an open-source messaging and streaming broker used across cloud, on-premises, and local environments. It supports workloads ranging from service decoupling and RPC to streaming pipelines and IoT systems. In many of those deployments, the broker is part of the security boundary, whether operators treat it that way or not.
Our Appsec agent reviewed the points where handling, authorization, metadata, and the connection state intersect. As a result of this work, we’ve identified 4 associated vulnerabilities.
All code excerpts below come from rabbitmq-server commit 83866edcc995602f546f3c4147078b3a610b0075, the vulnerable revision referenced by the advisories.
1. Existing consumers kept receiving messages after OAuth permissions changed
Advisory: GHSA-wmrr-4h5v-5ch7
RabbitMQ lets OAuth users update their credentials without closing their connection. It does this through connection.update_secret.
When the credentials change, RabbitMQ updates the user information stored on the connection and sends the new information to every open channel:
%% deps/rabbit/src/rabbit_reader.erl @ 1331-1356
handle_method0(#'connection.update_secret'{new_secret = NewSecret, reason = Reason},
State = #v1{connection =
#connection{user = User = #user{username = Username},
log_name = ConnName} = Conn,
sock = Sock}) when ?IS_RUNNING(State) ->
case rabbit_access_control:update_state(User, NewSecret) of
{ok, User1} ->
lists:foreach(fun(Ch) ->
_ = rabbit_channel:update_user_state(Ch, User1)
end, all_channels()),
ok = send_on_channel0(Sock, #'connection.update_secret_ok'{}),
State#v1{connection = Conn#connection{user = User1}};The channel then replaces its stored user information:
%% deps/rabbit/src/rabbit_channel.erl @ 708-709
handle_info({update_user_state, User}, State = #ch{cfg = Cfg}) ->
noreply(State#ch{cfg = Cfg#conf{user = User}}).The concern is that RabbitMQ didn’t recheck consumers that were created before the permissions changed.
A new consumer created after the permission was removed would be denied. A consumer that was already running could continue receiving messages.
RabbitMQ updated the user’s permissions, but did not update the consumer created under the old permissions.
2. A Khepri error could allow a topic operation that should be blocked
Advisory: GHSA-gpvw-75h5-3wvx
RabbitMQ stores topic permissions in Khepri.
When RabbitMQ looks up those permissions, the code returns the permission if the request succeeds. Every other response becomes undefined:
%% deps/rabbit/src/rabbit_db_user.erl @ 416-424
get_topic_permissions(Username, VHostName, ExchangeName) ->
Path = khepri_topic_permission_path(Username, VHostName, ExchangeName),
case rabbit_khepri:get(Path) of
{ok, TopicPermission} -> TopicPermission;
_ -> undefined
end.This means undefined can have two different meanings:
- No topic permission was found
- RabbitMQ could not complete the lookup because of an error or timeout
The authorization code then treats undefined as allowed:
%% deps/rabbit/src/rabbit_auth_backend_internal.erl @ 159-167
check_topic_access(#auth_user{username = Username},
#resource{virtual_host = VHostPath, name = Name, kind = topic},
Permission,
Context) ->
case rabbit_db_user:get_topic_permissions(Username, VHostPath, Name) of
undefined ->
true;RabbitMQ could not distinguish between a missing permission and a failed database request.
Because both cases returned the same value, a temporary Khepri error could allow a topic operation that RabbitMQ would normally block.
3. RabbitMQ checked its own address instead of the client’s address
Advisory: GHSA-36m6-588r-vqcw
RabbitMQ can limit certain users to loopback connections. A loopback connection comes from the same machine as the RabbitMQ server.
The default guest user is the main example.
RabbitMQ checks this rule through rabbit_net:is_loopback/1:
%% deps/rabbit/src/rabbit_access_control.erl @ 275-280
check_user_loopback(Username, SockOrAddr) ->
{ok, Users} = application:get_env(rabbit, loopback_users),
case rabbit_net:is_loopback(SockOrAddr)
orelse not lists:member(Username, Users) of
true -> ok;
false -> not_allowed
end.For socket connections, is_loopback/1 calls sockname/1:
%% deps/rabbit_common/src/rabbit_net.erl @ 278-281
is_loopback(Sock) when is_port(Sock) ; ?IS_SSL(Sock) ->
case sockname(Sock) of
{ok, {Addr, _Port}} -> is_loopback(Addr);
{error, _} -> false
end;sockname/1 returns the broker’s local address. It does not return the address of the client connecting to RabbitMQ.
This creates a problem when RabbitMQ runs behind a trusted proxy and its own listener uses a loopback address. A remote user can connect through the proxy, but RabbitMQ may see its own local address and treat the connection as local.
That could allow a remote user to log in with a loopback-only account. If the guest account still had broad permissions, the user could gain administrative access to the broker.
RabbitMQ had the correct rule, but checked the wrong address.
4. Direct Reply-To bindings could remain after deletion
Advisory: GHSA-5cq3-v9jx-p3x3
Direct Reply-To lets a client receive replies without creating a normal reply queue.
RabbitMQ handles this through rabbit_volatile_queue, a temporary internal queue type that is not stored like a normal queue.
When RabbitMQ creates the binding, it can create a Direct Reply-To target even if that target does not have a normal Khepri record:
%% deps/rabbit/src/rabbit_db_queue.erl @ 334-343
lookup_target(#resource{name = NameBin} = Name) ->
case rabbit_volatile_queue:is(NameBin) of
true ->
case rabbit_volatile_queue:new_target(Name) of
error -> not_found;
Target -> Target
end;
false ->
lookup_target0(Name)
end.The deletion code makes a different assumption. It expects both sides of the binding to exist in Khepri:
%% deps/rabbit/src/rabbit_db_binding.erl @ 198-215
case {lookup_resource_in_khepri_tx(SrcName),
lookup_resource_in_khepri_tx(DstName)} of
{[Src], [Dst]} ->
%% ...
ok = delete_in_khepri(Binding);
_Errs ->
ok
endIf RabbitMQ cannot find the Direct Reply-To target in Khepri, it returns ok without deleting the binding.
The binding could therefore remain active after RabbitMQ reported that it had been deleted. An attacker could use the remaining route to send messages into another client’s reply channel.
The creation code understood the temporary Direct Reply-To target. The deletion code did not.
What the Four Issues Have in Common
The four bugs affect different RabbitMQ features.
RabbitMQ made a security decision using information that was no longer correct or did not mean what the code assumed it meant.
| Decision | Information RabbitMQ used | Information it needed |
|---|---|---|
| Continue sending messages | Consumer created under the old OAuth permissions | The user’s current permissions |
| Allow a topic operation | undefined from Khepri | Whether the permission was missing or the lookup failed |
| Allow a loopback-only login | RabbitMQ’s local address | The remote client’s address |
| Delete a Direct Reply-To binding | Normal Khepri queue records | The temporary target used when the binding was created |
These issues are difficult to find by reading one function at a time. The important behavior happens when information moves from one part of RabbitMQ to another.
The code that updates a user must also consider existing consumers. The code that reads permissions must preserve the difference between “not found” and “request failed.” The code that limits remote users must check the client’s address. The code that creates a special type of binding must make sure the deletion path understands it too.
What RabbitMQ Operators Should Check
Exposure depends on which RabbitMQ features a deployment uses.
Organizations should check whether their brokers use:
- OAuth 2.0 with credentials updated on open connections
- Topic exchanges with topic permissions stored in Khepri
- Loopback-only users behind a PROXY protocol frontend
- Direct Reply-To for request and reply traffic
A deployment that does not use one of these features will not reach that specific vulnerable code path.
RabbitMQ should still be treated as part of the application’s security setup. If the broker controls which services or users can exchange messages, a RabbitMQ authorization failure can become an application security failure.
Patched Releases
RabbitMQ published fixes across the affected branches in these releases:
3.13.154.0.204.0.214.1.114.2.64.3.0
Not every release applies to every advisory. Operators should check the individual advisories, identify which RabbitMQ branch they use, and upgrade to the relevant patched version.
Disclosure Timeline
- April 3, 2026: Apex finds the first issues in RabbitMQ’s authorization and routing code.
- April 6, 2026: Apex privately reports additional issues to the RabbitMQ maintainers.
- June 24, 2026: RabbitMQ publishes the advisories and fixes.
Run This on Your Own Code
The issues we discovered above are an example of the type of reasoning our agents are built for: following a decision across modules and flagging where the assumptions go wrong. If you run RabbitMQ, or anything where the code deciding who gets access is spread across services, contact us - we’ll run the guardrails against your codebase in no time.
Advisory Links
- RabbitMQ security advisories index
- GHSA-wmrr-4h5v-5ch7: AMQP 0-9-1 in combination with OAuth 2: consumer persistence can lead to post-revocation message disclosure
- GHSA-gpvw-75h5-3wvx: Topic authorization can lead to cross-tenant routing-key bypass
- GHSA-36m6-588r-vqcw: AMQP 1.0, AMQP 0-9-1, Stream Protocol loopback enforcement can lead to remote guest sessions due to listener-address loopback checks
- GHSA-5cq3-v9jx-p3x3: Direct-reply-to binding persistence can lead to unauthorized reply-channel injection and persistent phantom