CVE-2026-44838
Description
RabbitMQ is a messaging and streaming broker. From 4.2.0 to before 4.2.4, RabbitMQ's MQTT plugin allows for topic-level authorization using regular expressions with variable substitution. Administrators can create patterns such as ^{client_id}-sensors$ to restrict user access to topics that include their client ID. However, the client_id is provided by the user in the MQTT CONNECT packet and is inserted into the regex pattern without escaping special regex characters. This flaw enables an authenticated MQTT user to inject regex operators to bypass authorization. This vulnerability is fixed in 4.2.4 and 4.3.0.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
RabbitMQ MQTT plugin before 4.2.4 allows authenticated users to bypass topic authorization by injecting regex metacharacters via client_id.
Vulnerability
In RabbitMQ versions 4.2.0 to 4.2.3, the MQTT plugin supports topic-level authorization using regular expressions with variable substitution (e.g., ^{client_id}-sensors$). The client_id is taken from the MQTT CONNECT packet and inserted into the pattern without escaping special regex characters. This allows an authenticated user to craft a client_id containing regex metacharacters (such as .*) to alter the intended regex, thereby bypassing authorization. The affected versions are from 4.2.0 up to (but not including) 4.2.4 and 4.3.0. The fix is introduced in versions 4.2.4 and 4.3.0 [1].
Exploitation
An attacker must be an authenticated MQTT user. When connecting, the attacker provides a malicious client_id containing regex metacharacters, such as .*. The MQTT plugin substitutes this client_id into the authorization regex pattern without escaping. For example, the pattern ^{client_id}-sensors$ becomes ^.*-sensors$, which matches any topic ending with -sensors. The attacker then publishes or subscribes to topics that should be restricted. No additional user interaction is required beyond the MQTT CONNECT packet [1].
Impact
Successful exploitation allows the attacker to bypass topic authorization checks. This can lead to unauthorized data access by subscribing to any topic, as well as unauthorized publishing to restricted topics, potentially enabling data exfiltration or injection of malicious messages. The impact is limited to authenticated users and the MQTT protocol, but it undermines the intended topic permission model [1].
Mitigation
Upgrade to RabbitMQ versions 4.2.4 or 4.3.0, which contain the fix. As of the publication date (2026-05-27), the fix is available. Workarounds include disabling the MQTT plugin, avoiding variable expansion in topic authorization patterns, or using separate users per client instead of relying on variable substitution. If upgrading is not possible, administrators should review and limit the use of client_id-based patterns [1].
AI Insight generated on May 27, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2(expand)+ 1 more
- (no CPE)
- (no CPE)range: >=4.2.0, <4.2.4
Patches
1df0fb8eb1023Escape '{variables}' in topic permissions
4 files changed · +129 −2
deps/rabbitmq_mqtt/test/auth_SUITE.erl+19 −0 modified@@ -113,6 +113,7 @@ sub_groups() -> topic_read_permission, topic_write_permission, topic_write_permission_variable_expansion, + topic_write_permission_client_id_regex_not_injected, loopback_user_connects_from_remote_host, connect_permission ] @@ -1118,6 +1119,24 @@ topic_write_permission_variable_expansion(Config) -> ]), ok. +topic_write_permission_client_id_regex_not_injected(Config) -> + set_permissions(".*", ".*", ".*", Config), + set_topic_permissions("^{client_id}-sensors$", ".*", Config), + User = ?config(mqtt_user, Config), + CraftedClientId = <<".*">>, + {ok, C} = connect_user(User, ?config(mqtt_password, Config), Config, CraftedClientId), + {ok, _} = emqtt:connect(C), + ?assertMatch({ok, _}, emqtt:publish(C, <<".*-sensors">>, <<"payload">>, qos1)), + unlink(C), + ?assertMatch({error, _}, emqtt:publish(C, <<"anything-sensors">>, <<"payload">>, qos1)), + wait_log(Config, + [?FAIL_IF_CRASH_LOG + ,{["MQTT topic access refused", + "MQTT connection .* is closing due to an authorization failure"], + fun () -> stop end} + ]), + ok. + loopback_user_connects_from_remote_host(Config) -> set_permissions(".*", ".*", ".*", Config), {ok, C} = connect_anonymous(Config),
deps/rabbitmq_stomp/test/topic_SUITE.erl+31 −0 modified@@ -21,6 +21,7 @@ groups() -> Tests = [ publish_topic_authorisation, subscribe_topic_authorisation, + publish_topic_authorisation_regex_not_injected, change_default_topic_exchange ], @@ -133,6 +134,36 @@ subscribe_topic_authorisation(Config) -> "access_refused" = proplists:get_value("message", Hdrs2), ok. +publish_topic_authorisation_regex_not_injected(Config) -> + Username = <<".*">>, + Password = <<"pass">>, + rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_auth_backend_internal, add_user, + [Username, Password, <<"acting-user">>]), + rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_auth_backend_internal, set_permissions, [ + Username, <<"/">>, <<".*">>, <<".*">>, <<".*">>, <<"acting-user">>]), + rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_auth_backend_internal, set_topic_permissions, [ + Username, <<"/">>, <<"amq.topic">>, <<"^{username}\\.Authorised">>, <<"^{username}\\.Authorised">>, <<"acting-user">>]), + Version = ?config(version, Config), + StompPort = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_stomp), + {ok, ClientRegex} = rabbit_stomp_client:connect(Version, ".*", "pass", StompPort), + + rabbit_stomp_client:send( + ClientRegex, "SUBSCRIBE", [{"destination", "/topic/.*.Authorised"}]), + rabbit_stomp_client:send( + ClientRegex, "SEND", [{"destination", "/topic/.*.Authorised"}], ["allowed"]), + {ok, _Client1, _, Body} = stomp_receive(ClientRegex, "MESSAGE"), + [<<"allowed">>] = Body, + + rabbit_stomp_client:send( + ClientRegex, "SEND", [{"destination", "/topic/injected.Authorised"}], ["denied"]), + {ok, _Client2, Hdrs2, _} = stomp_receive(ClientRegex, "ERROR"), + "access_refused" = proplists:get_value("message", Hdrs2), + + rabbit_stomp_client:disconnect(ClientRegex), + rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_auth_backend_internal, delete_user, + [Username, <<"acting-user">>]), + ok. + change_default_topic_exchange(Config) -> Channel = ?config(amqp_channel, Config), Version = ?config(version, Config),
deps/rabbit/src/rabbit_auth_backend_internal.erl+14 −1 modified@@ -184,12 +184,25 @@ expand_topic_permission(Permission, ToExpand) when is_map(ToExpand) -> Closing = <<"}">>, ReplaceFun = fun(K, V, Acc) -> Placeholder = <<Opening/binary, K/binary, Closing/binary>>, - binary:replace(Acc, Placeholder, V, [global]) + binary:replace(Acc, Placeholder, escape_regex(V), [global]) end, maps:fold(ReplaceFun, Permission, ToExpand); expand_topic_permission(Permission, _ToExpand) -> Permission. +-spec escape_regex(binary()) -> binary(). +escape_regex(Bin) when is_binary(Bin) -> + << <<(escape_regex_char(C))/binary>> || <<C>> <= Bin >>. + +escape_regex_char(C) + when C =:= $\\; C =:= $^; C =:= $$; C =:= $.; + C =:= $|; C =:= $?; C =:= $*; C =:= $+; + C =:= $(; C =:= $); C =:= $[; C =:= $]; + C =:= ${; C =:= $} -> + <<$\\, C>>; +escape_regex_char(C) -> + <<C>>. + permission_index(configure) -> #permission.configure; permission_index(write) -> #permission.write; permission_index(read) -> #permission.read.
deps/rabbit/test/unit_access_control_SUITE.erl+65 −1 modified@@ -7,6 +7,7 @@ -module(unit_access_control_SUITE). +-include_lib("proper/include/proper.hrl"). -include_lib("common_test/include/ct.hrl"). -include_lib("amqp_client/include/amqp_client.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -23,7 +24,8 @@ groups() -> [ {parallel_tests, [parallel], [ password_hashing, - version_negotiation + version_negotiation, + expand_topic_permission_prop ]}, {sequential_tests, [], [ login_with_credentials_but_no_password, @@ -283,8 +285,70 @@ auth_backend_internal_expand_topic_permission(_Config) -> <<"services/{vhost}/accounts/{username}/notifications">>, #{} ), + + %% value with special characters + <<"^\\.\\*-sensors$">> = + rabbit_auth_backend_internal:expand_topic_permission( + <<"^{client_id}-sensors$">>, + #{<<"client_id">> => <<".*">>} + ), + %% value with all special characters + <<"^\\\\\\^\\$\\.\\|\\?\\*\\+\\(\\)\\[\\]\\{\\}$">> = + rabbit_auth_backend_internal:expand_topic_permission( + <<"^{client_id}$">>, + #{<<"client_id">> => <<"\\^$.|?*+()[]{}">>} + ), + %% expanded pattern matches the value literally + Pattern = rabbit_auth_backend_internal:expand_topic_permission( + <<"^{client_id}-sensors$">>, + #{<<"client_id">> => <<".*">>} + ), + nomatch = re:run(<<"anything-sensors">>, Pattern, [{capture, none}]), + match = re:run(<<".*-sensors">>, Pattern, [{capture, none}]), + %% value with brackets and pipe + PatternPipe = rabbit_auth_backend_internal:expand_topic_permission( + <<"^{client_id}$">>, + #{<<"client_id">> => <<"a]|[b">>} + ), + nomatch = re:run(<<"a">>, PatternPipe, [{capture, none}]), + nomatch = re:run(<<"b">>, PatternPipe, [{capture, none}]), + match = re:run(<<"a]|[b">>, PatternPipe, [{capture, none}]), + %% plain value + <<"^device123-sensors$">> = + rabbit_auth_backend_internal:expand_topic_permission( + <<"^{client_id}-sensors$">>, + #{<<"client_id">> => <<"device123">>} + ), + %% multiple variables + Result = rabbit_auth_backend_internal:expand_topic_permission( + <<"^{username}.{client_id}$">>, + #{<<"username">> => <<"user.name">>, <<"client_id">> => <<"id*1">>} + ), + match = re:run(<<"user.name.id*1">>, Result, [{capture, none}]), + nomatch = re:run(<<"userXname.idZ1">>, Result, [{capture, none}]), + %% empty value + <<"^-sensors$">> = + rabbit_auth_backend_internal:expand_topic_permission( + <<"^{client_id}-sensors$">>, + #{<<"client_id">> => <<>>} + ), ok. +expand_topic_permission_prop(_Config) -> + Property = fun () -> prop_expand_topic_permission_matches_literally() end, + rabbit_ct_proper_helpers:run_proper(Property, [], 1000). + +prop_expand_topic_permission_matches_literally() -> + ?FORALL(V, binary(), + begin + Pattern = rabbit_auth_backend_internal:expand_topic_permission( + <<"^{x}$">>, #{<<"x">> => V}), + case re:run(V, Pattern, [{capture, none}]) of + match -> true; + nomatch -> false + end + end). + %% Test AMQP 1.0 §2.2 version_negotiation(Config) -> ok = rabbit_ct_broker_helpers:rpc(Config, ?MODULE, version_negotiation1, [Config]).
Vulnerability mechanics
Root cause
"Missing regex-escaping of user-supplied variable values when substituting them into topic permission patterns allows regex injection."
Attack vector
An authenticated MQTT (or STOMP) user can craft a `client_id` (or `username`) containing regex metacharacters such as `.*`, `+`, `|`, or `[]`. When the administrator has configured a topic permission pattern like `^{client_id}-sensors$`, the backend substitutes the malicious client_id directly into the regex without escaping. For example, a client_id of `.*` expands the pattern to `^.*-sensors$`, which matches any topic ending in `-sensors`, bypassing the intended per-user restriction. The attacker must be authenticated and the administrator must have defined a topic permission using variable substitution [patch_id=2691375].
Affected code
The vulnerability resides in `deps/rabbit/src/rabbit_auth_backend_internal.erl`, specifically in the `expand_topic_permission/2` function. This function substitutes `{client_id}` (and other `{variable}`) placeholders in topic permission patterns with the raw user-supplied value, without escaping regex-special characters. The MQTT plugin (`deps/rabbitmq_mqtt`) and STOMP plugin (`deps/rabbitmq_stomp`) both rely on this internal backend for topic authorization.
What the fix does
The patch adds a new `escape_regex/1` function in `rabbit_auth_backend_internal.erl` that prepends a backslash to each regex-special character (`\ ^ $ . | ? * + ( ) [ ] { }`) before substituting the variable value into the pattern. The `expand_topic_permission/2` function now calls `escape_regex(V)` instead of using the raw value `V`. The patch also adds property-based tests (`expand_topic_permission_prop`) and integration tests for MQTT and STOMP to verify that injected regex patterns no longer match unintended topics [patch_id=2691375].
Preconditions
- configThe administrator must have configured a topic permission pattern that uses variable substitution (e.g., {client_id} or {username}).
- authThe attacker must be an authenticated MQTT or STOMP user.
- inputThe attacker must be able to control the value substituted into the pattern (e.g., client_id in the MQTT CONNECT packet).
Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
1News mentions
0No linked articles in our index yet.