Securing MQTT on Embedded Devices: Resource Limits & Authentication Fixes in PicoW ClockMaster
Introduction
When we think about security vulnerabilities, our minds often jump to web applications, APIs, or cloud infrastructure. But some of the most impactful—and most overlooked—vulnerabilities live deep inside embedded firmware running on microcontrollers like the Raspberry Pi Pico W. These devices are small, but they often control physical actuators: motors, LEDs, clocks, and more. A security flaw here doesn't just leak data—it can move things in the real world.
This post dives into two related vulnerabilities discovered and patched in the PicoW ClockMaster firmware's MQTT client (mqtt.c). Together, they represent a classic pairing of resource exhaustion and unauthenticated command injection that is unfortunately common in IoT development. Understanding these issues—and how they were fixed—can help you build more resilient embedded systems.
The Vulnerability Explained
Vulnerability 1: No Resource Limits on File Import (V-004)
The file import functionality in mqtt.c loaded entire files into memory without any size validation. It also parsed incoming JSON payloads without imposing any depth or complexity limits.
Why is this dangerous?
Microcontrollers like the Pico W operate with severely constrained resources—often just 264KB of RAM. When you load a file without checking its size first, or parse JSON without bounding recursion depth, a single malicious input can exhaust available memory and crash the device.
This class of vulnerability is known as a Resource Exhaustion or Denial of Service (DoS) attack, catalogued under CWE-400: Uncontrolled Resource Consumption.
Attack Scenarios:
Consider an attacker who can send a crafted MQTT message containing a deeply nested JSON payload:
{
"a": {
"b": {
"c": {
"d": {
... // thousands of levels deep
}
}
}
}
}
A recursive JSON parser with no depth limit will keep allocating stack frames until the device runs out of memory and crashes—or worse, overwrites adjacent memory regions (stack overflow → potential code execution on some architectures).
Similarly, an import file with millions of entries:
[
{"entry": 1},
{"entry": 2},
... // 1,000,000 entries
]
...would exhaust heap memory long before processing completes, hanging or crashing the firmware.
Real-World Impact:
- Clock and motor control becomes unavailable (denial of service)
- Repeated crashes can wear out flash storage (on devices using wear-leveling)
- On some architectures, memory corruption from stack overflow can lead to arbitrary code execution
Vulnerability 2: Unauthenticated MQTT Broker Connection
The MQTT client established connections to the broker for clock and motor control without TLS encryption or client authentication. MQTT, by default, is a plaintext protocol—and without authentication, it's essentially an open door.
The MQTT Threat Model on a Local Network:
MQTT brokers typically listen on port 1883 (plaintext) or 8883 (TLS). When a device connects without credentials or certificate validation, anyone on the same network segment can:
- Subscribe to control topics and passively observe all commands being sent to the device
- Publish spoofed commands that the device will execute as if they came from a legitimate controller
Legitimate Controller Attacker (same WiFi)
| |
| PUBLISH /clock/motor/pos | PUBLISH /clock/motor/pos
| {"angle": 90} | {"angle": 9999}
v v
[ MQTT Broker ] <------------------+
|
v
[ PicoW ClockMaster ]
|
v
Motor moves to 9999° ← DANGER
Real-World Impact:
- An attacker on the same WiFi network (coffee shop, shared office, compromised home router) can send arbitrary motor position commands
- LED patterns can be spoofed or disrupted
- Clock state can be manipulated without authorization
- All communication is visible in plaintext to any passive network observer
This falls under CWE-306: Missing Authentication for Critical Function and CWE-319: Cleartext Transmission of Sensitive Information.
The Fix
The patch addresses both vulnerabilities with targeted, practical changes to mqtt.c.
Fix 1: Enforcing Resource Limits on File Import
Before (vulnerable pattern):
// No size check — loads entire file regardless of size
char *buffer = malloc(file_size);
fread(buffer, 1, file_size, fp);
// No depth limit — parses arbitrary JSON complexity
cJSON *root = cJSON_Parse(buffer);
process_json(root); // recursive, unbounded
After (hardened pattern):
#define MAX_IMPORT_FILE_SIZE (64 * 1024) // 64KB hard limit
#define MAX_JSON_DEPTH 10 // Reasonable nesting limit
// Validate file size before allocation
if (file_size > MAX_IMPORT_FILE_SIZE) {
log_error("Import file exceeds maximum allowed size (%d bytes)", MAX_IMPORT_FILE_SIZE);
fclose(fp);
return ERR_FILE_TOO_LARGE;
}
char *buffer = malloc(file_size + 1);
if (!buffer) {
fclose(fp);
return ERR_OUT_OF_MEMORY;
}
fread(buffer, 1, file_size, fp);
buffer[file_size] = '\0';
// Parse with depth limit
cJSON *root = cJSON_ParseWithOpts(buffer, NULL, false);
if (!root || cjson_get_depth(root) > MAX_JSON_DEPTH) {
log_error("JSON exceeds maximum depth or is malformed");
cJSON_Delete(root);
free(buffer);
return ERR_INVALID_JSON;
}
What this achieves:
- Hard cap on file size prevents memory exhaustion before allocation
- JSON depth validation prevents stack overflow from recursive parsing
- Explicit error handling ensures resources are freed on all failure paths
Fix 2: Adding MQTT Authentication and TLS
Before (vulnerable pattern):
// Unauthenticated, plaintext connection
mqtt_client_connect(client, broker_ip, 1883, NULL);
// No credentials, no TLS, no certificate validation
After (hardened pattern):
// TLS-enabled connection on port 8883
struct mqtt_connect_client_info_t ci = {
.client_id = DEVICE_CLIENT_ID,
.client_user = CONFIG_MQTT_USERNAME,
.client_pass = CONFIG_MQTT_PASSWORD,
.keep_alive = 60,
.tls_config = &tls_cfg, // TLS context with CA cert pinned
};
// Connect on TLS port
err = mqtt_client_connect(client, &broker_addr, 8883,
mqtt_connection_cb, NULL, &ci);
if (err != ERR_OK) {
log_error("MQTT connection failed: %d", err);
return err;
}
What this achieves:
- Authentication: Username/password credentials prevent unauthorized clients from connecting
- Encryption: TLS on port 8883 ensures all commands are encrypted in transit
- Certificate pinning (recommended): Validates the broker's identity, preventing man-in-the-middle attacks
Prevention & Best Practices
This vulnerability pair is common in IoT firmware. Here's how to systematically prevent similar issues in your projects:
1. Always Validate Input Size Before Processing
// Rule of thumb: validate BEFORE allocating
if (input_size == 0 || input_size > MAX_SAFE_SIZE) {
return ERR_INVALID_INPUT;
}
For embedded systems, define your maximum sizes based on available RAM, not theoretical limits. Leave headroom for the stack and other runtime allocations.
2. Bound All Recursive Operations
Any recursive algorithm (JSON parsing, XML parsing, directory traversal) needs an explicit depth limit:
void process_node(Node *n, int depth) {
if (depth > MAX_DEPTH) {
log_warn("Maximum recursion depth exceeded");
return;
}
// ... process ...
process_node(n->child, depth + 1);
}
3. Use TLS for All IoT Control Channels
MQTT without TLS is plaintext by definition. For any device that controls physical actuators:
- Use port 8883 (MQTT over TLS) instead of 1883
- Validate broker certificates (prevent MITM)
- Use per-device credentials or X.509 client certificates
- Consider certificate pinning for high-security applications
4. Apply the Principle of Least Privilege to MQTT Topics
Structure your topic ACLs so devices can only publish/subscribe to their own topics:
# Example Mosquitto ACL
user device_pico_001
topic readwrite clock/device_001/#
topic deny clock/#
5. Implement Watchdog Timers
On embedded systems, a hardware watchdog timer can recover from hangs caused by resource exhaustion:
// Kick the watchdog periodically in your main loop
watchdog_update();
6. Static Analysis and Fuzzing
- Use cppcheck or clang-analyzer to catch unbounded memory operations
- Fuzz your JSON/file parsers with tools like AFL++ before deployment
- Review all
malloc()calls: is the size user-controlled? Is there a maximum?
Relevant Standards and References
| Standard | Relevance |
|---|---|
| CWE-400 | Uncontrolled Resource Consumption |
| CWE-306 | Missing Authentication for Critical Function |
| CWE-319 | Cleartext Transmission of Sensitive Information |
| OWASP IoT Top 10 - I2 | Insecure Network Services |
| OWASP IoT Top 10 - I7 | Insecure Data Transfer and Storage |
| NIST SP 800-213 | IoT Device Cybersecurity Guidance |
Conclusion
The vulnerabilities patched in PicoW ClockMaster's mqtt.c are a microcosm of the broader IoT security challenge: resource-constrained devices controlling physical systems, connected to networks, with security often treated as an afterthought.
The key takeaways from this fix are:
- Never trust input size — validate before you allocate, especially on memory-constrained devices
- Bound recursive operations — JSON depth limits are not optional on embedded systems
- MQTT without authentication is an open invitation — always use TLS and credentials for control channels
- Physical actuators raise the stakes — a compromised motor controller isn't just a data breach; it's a safety issue
Security in embedded firmware doesn't require exotic techniques. It requires the same disciplined input validation, authentication, and encryption practices we apply to web and cloud systems—adapted to the constraints of the hardware. The patch here is small in terms of lines of code, but significant in terms of the attack surface it closes.
If you're building IoT firmware, audit your MQTT connections today. Check every malloc() call with a user-influenced size. Add depth limits to your parsers. These small investments pay dividends in reliability and security for the lifetime of your deployed devices.
Fixed by OrbisAI Security — automated vulnerability detection and remediation for modern codebases.