Summary
A malicious MQTT 5 broker can crash an MQTT.js client that did not advertise a receive Topic Alias Maximum. In the MQTT 5 PUBLISH handler, a broker-supplied Topic Alias is accepted even when topicAliasRecv was never initialized, so a normal inbound PUBLISH packet reaches an undefined-object dereference and escapes as an uncaught TypeError. For Node.js applications using MQTT.js as a client, this can turn a broker-controlled packet into process termination and client-side denial of service.
Affected
Root cause
The client constructor only initializes topicAliasRecv when options.properties.topicAliasMaximum is present and greater than zero (src/lib/client.ts:645). A client using MQTT 5 defaults without that option still routes broker network data through the parser: incoming bytes are parsed in writable._write (src/lib/client.ts:815), parsed packets are queued by the parser callback (src/lib/client.ts:784), and each queued packet is dispatched through handlePacket (src/lib/client.ts:793). handlePacket sends packets with cmd === 'publish' to handlePublish (src/lib/handlers/index.ts:28, src/lib/handlers/index.ts:31). In handlePublish, the MQTT 5 branch reads packet.properties.topicAlias from the broker-controlled PUBLISH packet (src/lib/handlers/publish.ts:38, src/lib/handlers/publish.ts:40, src/lib/handlers/publish.ts:43). When the packet has a non-empty topic and a defined alias, the handler calls client['topicAliasRecv'].put(topic, alias) without first checking whether topicAliasRecv exists (src/lib/handlers/publish.ts:77). If the client did not advertise a receive maximum, that object is undefined and the public receive path throws before MQTT.js can convert the invalid alias into a handled protocol error.
Reproduction
INT-mqtt-mqttjs-v5-unsolicited-topic-alias-crash.zip
run 1: exit=1 timed_out=False duration=0.26s matched=True phase=oracle fingerprint="TypeError: Cannot read properties of undefined (reading 'put')"
run 2: exit=1 timed_out=False duration=0.26s matched=True phase=oracle fingerprint="TypeError: Cannot read properties of undefined (reading 'put')"
run 3: exit=1 timed_out=False duration=0.26s matched=True phase=oracle fingerprint="TypeError: Cannot read properties of undefined (reading 'put')"
The fingerprint is the uncaught exception raised when the broker-supplied PUBLISH packet reaches client.topicAliasRecv.put(...) while topicAliasRecv is undefined. A non-zero exit without this TypeError would be a setup or build failure, not evidence that this MQTT 5 Topic Alias path was reached.
Impact
The attacker is a remote unauthenticated malicious MQTT broker, or a broker-equivalent network peer, that the victim MQTT.js client connects to over an attacker-reachable network. The victim application must use MQTT.js with MQTT protocolVersion: 5 and omit options.properties.topicAliasMaximum, which is the stock default exercised by the PoC; the broker then only needs to complete CONNACK and send one PUBLISH packet with a non-empty topic and Topic Alias property under any configured maximum packet size. MQTT.js has no blocking guard in this configuration, so the TypeError escapes as an uncaught exception. In a typical Node.js service or worker without a process-wide uncaughtException recovery policy, that terminates the process, disconnects the MQTT client, and can be repeated on reconnect to keep the client-side service unavailable until the broker endpoint or client configuration changes.
Suggested fix
--- a/src/lib/handlers/publish.ts
+++ b/src/lib/handlers/publish.ts
@@ -41,10 +41,21 @@
alias = packet.properties.topicAlias
}
if (typeof alias !== 'undefined') {
+ const topicAliasRecv = client['topicAliasRecv']
+ if (!topicAliasRecv) {
+ client.log(
+ 'handlePublish :: topic alias out of range. alias: %d',
+ alias,
+ )
+ client.emit(
+ 'error',
+ new Error('Received Topic Alias is out of range'),
+ )
+ return
+ }
if (topic.length === 0) {
if (alias > 0 && alias <= 0xffff) {
- const gotTopic =
- client['topicAliasRecv'].getTopicByAlias(alias)
+ const gotTopic = topicAliasRecv.getTopicByAlias(alias)
if (gotTopic) {
topic = gotTopic
client.log(
@@ -74,7 +85,7 @@
)
return
}
- } else if (client['topicAliasRecv'].put(topic, alias)) {
+ } else if (topicAliasRecv.put(topic, alias)) {
client.log(
'handlePublish :: registered topic: %s - alias: %d',
topic,
--- a/build/lib/handlers/publish.js
+++ b/build/lib/handlers/publish.js
@@ -15,9 +15,15 @@
alias = packet.properties.topicAlias;
}
if (typeof alias !== 'undefined') {
+ const topicAliasRecv = client['topicAliasRecv'];
+ if (!topicAliasRecv) {
+ client.log('handlePublish :: topic alias out of range. alias: %d', alias);
+ client.emit('error', new Error('Received Topic Alias is out of range'));
+ return;
+ }
if (topic.length === 0) {
if (alias > 0 && alias <= 0xffff) {
- const gotTopic = client['topicAliasRecv'].getTopicByAlias(alias);
+ const gotTopic = topicAliasRecv.getTopicByAlias(alias);
if (gotTopic) {
topic = gotTopic;
client.log('handlePublish :: topic complemented by alias. topic: %s - alias: %d', topic, alias);
@@ -34,7 +40,7 @@
return;
}
}
- else if (client['topicAliasRecv'].put(topic, alias)) {
+ else if (topicAliasRecv.put(topic, alias)) {
client.log('handlePublish :: registered topic: %s - alias: %d', topic, alias);
}
else {
Reported by Team Atlanta.
Additional affected branch (from duplicate report GHSA-5vmf-27rw-vgmp)
The same uninitialized topicAliasRecv is also reachable through the
empty-topic branch, not only the put() call at src/lib/handlers/publish.ts:77:
topic: '', properties: { topicAlias: 1 }
-> client['topicAliasRecv'].getTopicByAlias(alias) // publish.ts:47
-> TypeError: Cannot read properties of undefined (reading 'getTopicByAlias')
Both derefs are covered by the fix, which guards the whole alias block before
either branch is entered. Independently reported by @afldl.
Fix
Fixed in mqtt 5.16.0 (commit 7d07757a14, PR #2060).
Upgrade with npm i mqtt@^5.16.0.
Affected: everything <= 5.15.2, which includes the whole v4 line. There is no backport — 5.16.0 is the only fixed release.
Consumer-visible changes
- A client receiving an invalid Topic Alias now emits
error and drops the connection, where before it crashed the process. Reconnect behaviour is unchanged.
- The unregistered-alias case emits reason code
0x82 (Protocol Error) instead of 0x94.
PacketPump is exported from src/lib/shared.ts.
Workaround
None that is complete. An uncaughtException handler keeps the process alive but leaves the client in an undefined state and the broker can repeat the packet on every reconnect. Connecting over mqtts:// removes an on-path injector but not a malicious or compromised broker. Upgrade.
Summary
A malicious MQTT 5 broker can crash an MQTT.js client that did not advertise a receive Topic Alias Maximum. In the MQTT 5 PUBLISH handler, a broker-supplied Topic Alias is accepted even when
topicAliasRecvwas never initialized, so a normal inbound PUBLISH packet reaches an undefined-object dereference and escapes as an uncaught TypeError. For Node.js applications using MQTT.js as a client, this can turn a broker-controlled packet into process termination and client-side denial of service.Affected
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HRoot cause
The client constructor only initializes
topicAliasRecvwhenoptions.properties.topicAliasMaximumis present and greater than zero (src/lib/client.ts:645). A client using MQTT 5 defaults without that option still routes broker network data through the parser: incoming bytes are parsed inwritable._write(src/lib/client.ts:815), parsed packets are queued by the parser callback (src/lib/client.ts:784), and each queued packet is dispatched throughhandlePacket(src/lib/client.ts:793).handlePacketsends packets withcmd === 'publish'tohandlePublish(src/lib/handlers/index.ts:28,src/lib/handlers/index.ts:31). InhandlePublish, the MQTT 5 branch readspacket.properties.topicAliasfrom the broker-controlled PUBLISH packet (src/lib/handlers/publish.ts:38,src/lib/handlers/publish.ts:40,src/lib/handlers/publish.ts:43). When the packet has a non-empty topic and a defined alias, the handler callsclient['topicAliasRecv'].put(topic, alias)without first checking whethertopicAliasRecvexists (src/lib/handlers/publish.ts:77). If the client did not advertise a receive maximum, that object is undefined and the public receive path throws before MQTT.js can convert the invalid alias into a handled protocol error.Reproduction
INT-mqtt-mqttjs-v5-unsolicited-topic-alias-crash.zip
The fingerprint is the uncaught exception raised when the broker-supplied PUBLISH packet reaches
client.topicAliasRecv.put(...)whiletopicAliasRecvis undefined. A non-zero exit without this TypeError would be a setup or build failure, not evidence that this MQTT 5 Topic Alias path was reached.Impact
The attacker is a remote unauthenticated malicious MQTT broker, or a broker-equivalent network peer, that the victim MQTT.js client connects to over an attacker-reachable network. The victim application must use MQTT.js with MQTT
protocolVersion: 5and omitoptions.properties.topicAliasMaximum, which is the stock default exercised by the PoC; the broker then only needs to complete CONNACK and send one PUBLISH packet with a non-empty topic and Topic Alias property under any configured maximum packet size. MQTT.js has no blocking guard in this configuration, so the TypeError escapes as an uncaught exception. In a typical Node.js service or worker without a process-wideuncaughtExceptionrecovery policy, that terminates the process, disconnects the MQTT client, and can be repeated on reconnect to keep the client-side service unavailable until the broker endpoint or client configuration changes.Suggested fix
Reported by Team Atlanta.
Additional affected branch (from duplicate report GHSA-5vmf-27rw-vgmp)
The same uninitialized
topicAliasRecvis also reachable through theempty-topic branch, not only the
put()call atsrc/lib/handlers/publish.ts:77:Both derefs are covered by the fix, which guards the whole alias block before
either branch is entered. Independently reported by @afldl.
Fix
Fixed in mqtt 5.16.0 (commit
7d07757a14, PR #2060).Upgrade with
npm i mqtt@^5.16.0.Affected: everything
<= 5.15.2, which includes the whole v4 line. There is no backport — 5.16.0 is the only fixed release.Consumer-visible changes
errorand drops the connection, where before it crashed the process. Reconnect behaviour is unchanged.0x82(Protocol Error) instead of0x94.PacketPumpis exported fromsrc/lib/shared.ts.Workaround
None that is complete. An
uncaughtExceptionhandler keeps the process alive but leaves the client in an undefined state and the broker can repeat the packet on every reconnect. Connecting overmqtts://removes an on-path injector but not a malicious or compromised broker. Upgrade.