| Título | 78 xiaozhi-esp32 2aeecd4e014780ac15cfa4866906cca16267010d Denial of Service |
|---|
| Descripción | ## Vulnerability Title
Denial of Service via Hash-Key Confusion in MQTT Goodbye Handling
## Affected Component
`main/protocols/mqtt_protocol.cc`
Repository: https://github.com/78/xiaozhi-esp32
## Summary
An attacker with the ability to deliver MQTT control messages to a device can craft a `goodbye` payload whose effective application key is either an absent `session_id` or the victim's current `session_id`. The shutdown decision excludes MQTT topic and source context, causing application-level key confusion that leads to premature UDP audio-channel termination for users of the MQTT + UDP audio protocol, resulting in a Denial of Service (DoS).
## Technical Details
The vulnerability occurs because `MqttProtocol` computes an application-level message key from only `type == "goodbye"` and `session_id` matching logic, then uses that key for a security-relevant shutdown decision. The effective key does not include critical security fields, such as the incoming MQTT topic or the MQTT publisher/source context.
**Vulnerable Code Logic**
In `main/protocols/mqtt_protocol.cc`, the incoming message is evaluated. If the payload `type` is `goodbye`, it checks if the `session_id` is missing (`nullptr`) OR if it matches the stored `session_id_`. If either condition is met, it executes `CloseAudioChannel(false)`:
```cpp
if (strcmp(type->valuestring, "goodbye") == 0) {
auto session_id = cJSON_GetObjectItem(root, "session_id");
ESP_LOGI(TAG, "Received goodbye message, session_id: %s", session_id ? session_id->valuestring : "null");
if (session_id == nullptr || session_id_ == session_id->valuestring) {
auto alive = alive_; // Capture alive flag
Application::GetInstance().Schedule([this, alive]() {
if (*alive) {
// Server initiated goodbye, don't send goodbye back to avoid ping-pong
CloseAudioChannel(false);
}
});
}
}
```
Because a missing `session_id` acts as a wildcard, an attacker does not even need to guess the valid session ID to trigger the channel reset. Furthermore, the check ignores the incoming `topic` parameter, allowing messages delivered on incorrect or unauthorized topics to successfully trigger the session shutdown pathway.
**Conflicting Object Scenario**
Legitimate server message for the victim device:
```json
{
"topic": "devices/A/down",
"publisher": "trusted-server",
"payload": {
"type": "goodbye",
"session_id": "sess-42"
}
}
```
Attacker-controlled or misrouted message:
```json
{
"topic": "devices/B/down",
"publisher": "attacker-or-compromised-client",
"payload": {
"type": "goodbye"
}
}
```
Both messages drive the exact same shutdown path because the missing `session_id` satisfies the wildcard branch, breaking the expected security boundaries of the MQTT context.
## Impact
This vulnerability allows attackers to:
- Instantly terminate active MQTT + UDP audio sessions by injecting or misrouting a malformed `goodbye` control message.
- Cause a persistent Denial of Service (DoS) on target devices by repeatedly transmitting `{"type":"goodbye"}` payloads.
- Exploit deployments that utilize broad MQTT subscriptions, shared broker credentials, or misconfigured ACLs where message isolation is expected but not enforced by the firmware.
## Proof of Concept
The following minimal script demonstrates the broken security invariant by modeling the vulnerable handler's decision rules. It shows how non-equivalent MQTT contexts trigger the same premature channel termination.
```python
#!/usr/bin/env python3
"""Minimal PoC for application-level key confusion in MQTT goodbye handling."""
class MqttProtocolModel:
def __init__(self):
self.session_id = "sess-42"
self.udp_open = True
self.close_calls = 0
def effective_key(self, message):
payload = message["payload"]
if payload.get("type") != "goodbye":
return None
incoming_session = payload.get("session_id")
if incoming_session is None:
return "goodbye:any-session"
if incoming_session == self.session_id:
return f"goodbye:session:{self.session_id}"
return None
def on_message(self, message):
key = self.effective_key(message)
if key is not None:
self.udp_open = False
self.close_calls += 1
return key
victim_server_message = {
"topic": "devices/A/down",
"publisher": "trusted-server",
"payload": {"type": "goodbye", "session_id": "sess-42"},
}
attacker_message = {
"topic": "devices/B/down",
"publisher": "attacker-or-misrouted-client",
"payload": {"type": "goodbye"},
}
assert victim_server_message != attacker_message
protocol = MqttProtocolModel()
attacker_key = protocol.on_message(attacker_message)
print("attacker effective key:", attacker_key)
print("udp_open after attacker message:", protocol.udp_open)
print("close_calls:", protocol.close_calls)
```
*Observed output: `udp_open after attacker message: False`, confirming that the unauthenticated, misrouted attacker message successfully triggers audio closure.*
## Remediation
Modify the logic in `MqttProtocol::OnMessage` to enforce strict validation on the `goodbye` payload and context. Reject payloads missing a valid string-based `session_id` and ensure that the incoming topic matches the device's designated inbound downlink topic.
Example remediation layout:
```cpp
if (strcmp(type->valuestring, "goodbye") == 0) {
cJSON* session_id = cJSON_GetObjectItem(root, "session_id");
if (!cJSON_IsString(session_id)) {
cJSON_Delete(root);
return; // Reject wildcard/missing session IDs
}
if (!IsExpectedInboundTopic(topic)) {
cJSON_Delete(root);
return; // Reject messages delivered on unexpected topics
}
if (session_id_ == session_id->valuestring) {
CloseAudioChannel(false);
}
}
```
Additional mitigations:
1. Reject Wildcards: Disallow any processing of `goodbye` payloads that lack explicit parameter validation.
2. Broker-Side Authorization: Implement strict broker ACL rules ensuring that only authenticated server components can write to device downlink branches.
3. Complete Field Coverage: Build session validation checks that explicitly bind the context of the session ID together with the expected transport boundaries.
## References
- Vulnerable MQTT callback handling: `https://github.com/78/xiaozhi-esp32/blob/2aeecd4e014780ac15cfa4866906cca16267010d/main/protocols/mqtt_protocol.cc#L100-L126`
- Audio channel termination path: `https://github.com/78/xiaozhi-esp32/blob/2aeecd4e014780ac15cfa4866906cca16267010d/main/protocols/mqtt_protocol.cc#L192-L212`
- Protocol specification documentation: `https://github.com/78/xiaozhi-esp32/blob/2aeecd4e014780ac15cfa4866906cca16267010d/docs/mqtt-udp.md#L160-L177` |
|---|
| Fuente | ⚠️ https://github.com/78/xiaozhi-esp32/issues/2022 |
|---|
| Usuario | dem0000 (UID 98390) |
|---|
| Sumisión | 2026-05-27 09:20 (hace 1 mes) |
|---|
| Moderación | 2026-06-27 18:04 (1 month later) |
|---|
| Estado | Aceptado |
|---|
| Entrada de VulDB | 374488 [78 xiaozhi-esp32 hasta 2.2.6 MQTT Goodbye mqtt_protocol.cc Application::GetInstance session_id denegación de servicio] |
|---|
| Puntos | 20 |
|---|