Skip to content
• 7 min read

MQTT Debug Topic Leaks Explained: When Your Device Keeps a Diary Anyone Can Read

A retained MQTT debug topic is the IoT equivalent of leaving the master key on a public noticeboard that never gets cleaned. Here is how firmware analysis, weak broker credentials, and one forgotten retain flag turn a thermostat into a leaky vault.

#MQTT #IoT #Firmware Analysis #Retained Messages #Weak Credentials
MQTT Debug Topic Leaks Explained: When Your Device Keeps a Diary Anyone Can Read hero illustration
Listen to Research
AI Narration
0:00 / --:--

Imagine you walk into a smart building and the thermostat is chatting away on a public channel. Not just temperature readings. It is also publishing its own device ID, firmware version, provision token, and a nice little note that says “here is the sensitive data, please take it.” The message is marked retain, so every new subscriber gets the full dump the moment they connect. That is not a hypothetical. That is what happens when debug topics stay enabled in production and the broker treats every authenticated client like family.

This article walks through the underlying pattern: how a recovered IoT firmware reveals broker details and topic structure, how a weak password on the MQTT broker opens the door, and how a single retained debug message hands the attacker everything they need. No drama required. Just a device that never learned to stop talking.


What Is an MQTT Debug Topic Leak, Anyway?

MQTT is a lightweight publish/subscribe protocol built for constrained devices. Clients connect to a broker, subscribe to topics, and publish messages. Two features make it especially interesting from a security angle:

  1. Retained messages. When a publisher sets the retain flag, the broker stores the last message on that topic and delivers it immediately to any future subscriber. Useful for last-known state. Dangerous when the state is a secret.
  2. Hierarchical topics and wildcards. Topics look like building/floor3/thermostat/status. A subscriber can use # or + to grab entire subtrees. If the ACL is missing or too loose, one authenticated client can read everything.

A “debug topic” is simply a topic the firmware (or the server) uses for diagnostics: device ID, build flags, internal tokens, configuration dumps, sometimes even flags left over from development. When that topic is published with retain=1 and any authenticated user can subscribe to it, you have a classic information disclosure that survives reboots and client disconnects.

The vulnerability is rarely “MQTT is broken.” It is almost always the combination of:

  • Credentials that are weak, shared, or recoverable from firmware
  • Debug/diagnostic channels left enabled
  • Overly permissive topic ACLs (or no ACLs at all)
  • Retain used on data that should never be sticky

Think of it as the difference between a private log file and a sticky note on the office fridge. Same content. Very different audience.


Why Should You Care?

IoT deployments love MQTT. Thermostats, sensors, gateways, smart locks, industrial controllers. Many of them ship with:

  • Hardcoded or easily recovered broker hostnames and usernames
  • Passwords that never left the factory wordlist
  • Debug topics that were never gated behind a compile-time flag
  • Brokers that accept the same username for every device and give full topic access

Real consequences show up as:

  • Credential and token exposure. Provisioning tokens, OTA salts, device IDs, and internal notes travel on the wire and stay on the broker.
  • Lateral movement. One compromised device credential often reads data from every other device under the same ACL.
  • Persistence without persistence. Retained messages outlive the publisher. An attacker who connects once can collect the dump later.
  • Physical + network chain. Firmware recovered from a discarded or unattended unit becomes the map for the live broker.

If your building (or product) treats the MQTT broker as “just internal,” you are one weak password away from handing an outsider the entire device inventory and its secrets.


Worked Example: From Firmware to Retained Dump

Here is a generic, synthetic scenario that mirrors the common pattern.

You recover a firmware binary from a commercial thermostat. It is a 32-bit Xtensa (ESP-class) ELF, not stripped, still carrying debug symbols and readable strings. Standard triage:

file firmware.bin
strings firmware.bin | grep -E 'mqtt|broker|topic|debug|nest|device'
readelf -W -s firmware.bin | grep -E 'mqtt|debug|connect|pub'

Strings immediately surface topic templates and function names:

device/%s/status
device/%s/telemetry
device/%s/diag
device/%s/$debug
[MQTT] Connecting broker=%s user=%s
[NG-MQTT] Debug payload published to %s (retain=1)
BUG: password not stored in firmware — provisioned at manufacture

Further reverse engineering of the connect path shows the client authenticates with a fixed username and a NULL password in the firmware itself. The real password lives only on the broker side (provisioned at manufacture and never rotated). The debug publish routine builds a topic of the form device/<device-id>/$debug, packs a JSON object containing model, firmware version, provision token, and an internal note, then publishes it with the retain flag set.

The device ID and a few other secrets are stored as lightly obfuscated byte arrays (simple alternating XOR). Decoding them is trivial once the constant is recovered:

def decode(enc):
    return bytes(b ^ (0xAB if i % 2 == 0 else 0xCD) for i, b in enumerate(enc))

You now know the username, the topic hierarchy, and that a retained debug message exists. The missing piece is the broker password.

A short list of candidates derived from the product branding and common wordlists is enough. One of them authenticates. At that point a single subscription does the rest:

mosquitto_sub -h broker.example -p 1883 \
  -u shared_user -P recovered_password \
  -t 'device/#' -v

Among the retained messages is the debug payload:

{
  "device_id": "TH-2000-4f7a",
  "fw_version": "2.3.1",
  "debug_build": true,
  "provision_token": "a1b2c3d4",
  "note": "server-provisioned diagnostic data"
}

That is the entire chain: firmware triage → recoverable identity and topic map → weak shared password → retained debug topic → sensitive data.

Adjacent variants are easy to imagine. If the debug topic is not retained but is published periodically, a short-lived subscription still catches it. If ACLs restrict the exact device ID but allow a parent wildcard, the same leak occurs. If the password is a DJB2 hash of a known string, offline cracking replaces the online wordlist attempt. The core lesson stays the same.


Vulnerable Code Examples

MQTT client (conceptual C / ESP-style)

Vulnerable version

// device_id and username recovered from firmware; password is NULL in the binary
mqtt_client.setServer(broker_host, 1883);
mqtt_client.connect(device_id, "shared_user", NULL);  // real password lives only on broker

char debug_topic[64];
snprintf(debug_topic, sizeof(debug_topic), "device/%s/$debug", device_id);

char payload[256];
snprintf(payload, sizeof(payload),
    "{\"device_id\":\"%s\",\"fw\":\"%s\",\"token\":\"%08x\",\"note\":\"diag\"}",
    device_id, FW_VERSION, provision_token);

mqtt_client.publish(debug_topic, payload, true);  // retain = true
// Any client that authenticates as shared_user can later subscribe and receive this.

The retain flag turns a one-time diagnostic publish into permanent storage on the broker. Combined with a shared username and no per-device ACL, every future subscriber gets the dump.

Patched version

// Strong, unique credentials per device; password never NULL
mqtt_client.setServer(broker_host, 1883);
mqtt_client.connect(device_id, device_user, device_password);

// Debug topic only compiled in when DEBUG is defined, and never retained
#ifdef DEBUG
char debug_topic[64];
snprintf(debug_topic, sizeof(debug_topic), "device/%s/$debug", device_id);
mqtt_client.publish(debug_topic, payload, false);  // no retain
#endif

Even better: remove the debug publish path from production builds entirely and enforce topic ACLs on the broker so that a compromised device cannot read another device’s topics.

Broker-side ACL (Mosquitto example)

Vulnerable

user shared_user
topic readwrite #

Hardened

user device_TH2000_4f7a
topic read device/TH-2000-4f7a/#
topic write device/TH-2000-4f7a/telemetry
topic write device/TH-2000-4f7a/status
# no access to any $debug or other devices

Defense / How to Fix

  1. Treat debug topics as production risks. Gate them behind compile-time flags. Never ship retained diagnostic payloads that contain tokens, internal notes, or device inventory data.
  2. Use unique credentials per device. Shared usernames + factory passwords are a gift. Prefer certificate-based auth (mTLS) or short-lived tokens where possible.
  3. Enforce topic ACLs. A client should only read and write the topics that belong to it. Wildcards for administrative users only, and even then with care.
  4. Disable retain on sensitive data. Last-known temperature is fine. Last-known provision token is not.
  5. Rotate credentials and purge retained messages when a device is decommissioned or a password is suspected compromised. mosquitto_pub -n -r -t topic is the usual way to clear a retained message.
  6. Assume firmware will be recovered. Obfuscation (simple XOR, light packing) buys minutes, not security. Design as if an attacker already has the binary.
  7. Monitor the broker. Unexpected subscriptions to $debug, #, or administrative topics are worth alerting on.

Final Thoughts

MQTT is excellent at moving small messages between constrained devices. It is also excellent at quietly retaining those messages for whoever authenticates next. When the message is a debug dump and the authentication is a shared weak password recovered from firmware, the “smart” building starts looking a lot less smart.

The safest debug topic is the one that never ships. The second safest is the one that requires a unique credential, has no retain flag, and lives behind an ACL that treats every other client as untrusted.

Stop letting your devices keep a public diary.


References

⌘
Suggested Searches