From ccce7eb8e2326f4daca788ac1a023b4c1c4b17f6 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 13:14:45 -0700 Subject: [PATCH 01/31] F-3825 F-4050 F-4051 F-4052 F-4059 - Harden v5 unsubscribe reject and sub/unsub property cleanup --- src/mqtt_client.c | 28 +++++++++++++ src/mqtt_packet.c | 63 ++++++++++++++++++++-------- tests/test_broker_connect.c | 56 +++++++++++++++++++++++++ tests/test_mqtt_packet.c | 82 +++++++++++++++++++++++++++++++++++++ wolfmqtt/mqtt_types.h | 4 ++ 5 files changed, 217 insertions(+), 16 deletions(-) diff --git a/src/mqtt_client.c b/src/mqtt_client.c index f5bc6fac9..6a1aca208 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -2506,6 +2506,10 @@ int MqttClient_Subscribe(MqttClient *client, MqttSubscribe *subscribe) int MqttClient_Unsubscribe(MqttClient *client, MqttUnsubscribe *unsubscribe) { int rc; +#ifdef WOLFMQTT_V5 + int i; + word16 reason_count; +#endif /* Validate required arguments */ if (client == NULL || unsubscribe == NULL) { @@ -2595,6 +2599,25 @@ int MqttClient_Unsubscribe(MqttClient *client, MqttUnsubscribe *unsubscribe) #endif #ifdef WOLFMQTT_V5 + /* Detect broker rejection. A v5 UNSUBACK carries one reason code per + * topic filter; any code with the high bit set (>= 0x80) means the + * broker refused to remove that subscription, so the caller must not + * assume the filter is gone. Mirrors the SUBSCRIBE rejection path. */ + if (rc == MQTT_CODE_SUCCESS && + unsubscribe->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 && + unsubscribe->ack.reason_codes != NULL) { + reason_count = unsubscribe->ack.reason_code_count; + if (reason_count > (word16)unsubscribe->topic_count) { + reason_count = (word16)unsubscribe->topic_count; + } + for (i = 0; i < (int)reason_count; i++) { + if (unsubscribe->ack.reason_codes[i] & 0x80) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_UNSUBSCRIBE_REJECTED); + break; + } + } + } + if (unsubscribe->ack.props != NULL) { /* Release the allocated properties */ MqttClient_PropsFree(unsubscribe->ack.props); @@ -2891,6 +2914,11 @@ int MqttClient_Auth(MqttClient *client, MqttAuth* auth) } #endif + /* Scrub the decoded AUTH response from rx_buf. Its properties (e.g. the + * MQTT_PROP_AUTH_DATA SASL blob) point into rx_buf and linger there until + * the next read overwrites them, so zero the buffer before returning. */ + CLIENT_FORCE_ZERO(client->rx_buf, client->rx_buf_len); + /* reset state */ auth->stat.write = MQTT_MSG_BEGIN; diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 66eba6ce9..672a593b7 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -2425,26 +2425,30 @@ int MqttDecode_Subscribe(byte *rx_buf, int rx_buf_len, MqttSubscribe *subscribe) byte options; word16 filter_len = 0; if (subscribe->topic_count >= MAX_MQTT_TOPICS) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + break; } topic = &subscribe->topics[subscribe->topic_count]; tmp = MqttDecode_String(rx_payload, &topic->topic_filter, &filter_len, (word32)(rx_end - rx_payload)); if (tmp < 0) { - return tmp; + break; } if (rx_payload + tmp > rx_end) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + break; } /* [MQTT-4.7.3-1] / [MQTT-4.7.1-2] / [MQTT-4.7.1-3] Reject * empty filters and malformed wildcard placement. */ if (!MqttPacket_TopicFilterValid(topic->topic_filter, filter_len)) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + break; } rx_payload += tmp; if (rx_payload >= rx_end) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + break; } options = *rx_payload++; /* MQTT 3.1.1 section 3.8.3.1: bits 2-7 of the SUBSCRIBE options byte @@ -2461,7 +2465,8 @@ int MqttDecode_Subscribe(byte *rx_buf, int rx_buf_len, MqttSubscribe *subscribe) if ((options & 0xC0) != 0 || (options & 0x03) > MQTT_QOS_2 || ((options >> 4) & 0x03) == 0x03) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + break; } } else @@ -2469,7 +2474,8 @@ int MqttDecode_Subscribe(byte *rx_buf, int rx_buf_len, MqttSubscribe *subscribe) { if ((options & 0xFC) != 0 || (options & 0x03) > MQTT_QOS_2) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + break; } } topic->qos = (MqttQoS)(options & 0x03); @@ -2479,8 +2485,18 @@ int MqttDecode_Subscribe(byte *rx_buf, int rx_buf_len, MqttSubscribe *subscribe) /* [MQTT-3.8.3-3] The payload of a SUBSCRIBE packet MUST contain at * least one Topic Filter / QoS pair. v5 section 3.8.3 carries the same * minimum-cardinality requirement. */ - if (subscribe->topic_count == 0) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + if (tmp >= 0 && subscribe->topic_count == 0) { + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } + if (tmp < 0) { + #ifdef WOLFMQTT_V5 + /* Free any v5 properties decoded above so a malformed topic + * filter cannot leak the property list; the broker caller only + * frees props on the success path. */ + MqttProps_Free(subscribe->props); + subscribe->props = NULL; + #endif + return tmp; } } @@ -2783,23 +2799,26 @@ int MqttDecode_Unsubscribe(byte *rx_buf, int rx_buf_len, MqttUnsubscribe *unsubs MqttTopic *topic; word16 filter_len = 0; if (unsubscribe->topic_count >= MAX_MQTT_TOPICS) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + break; } topic = &unsubscribe->topics[unsubscribe->topic_count]; tmp = MqttDecode_String(rx_payload, &topic->topic_filter, &filter_len, (word32)(rx_end - rx_payload)); if (tmp < 0) { - return tmp; + break; } if (rx_payload + tmp > rx_end) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + break; } /* [MQTT-4.7.3-1] / [MQTT-4.7.1-2] / [MQTT-4.7.1-3]: an * UNSUBSCRIBE Topic Filter must obey the same syntax rules * as a SUBSCRIBE Topic Filter. */ if (!MqttPacket_TopicFilterValid(topic->topic_filter, filter_len)) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + break; } rx_payload += tmp; unsubscribe->topic_count++; @@ -2808,8 +2827,18 @@ int MqttDecode_Unsubscribe(byte *rx_buf, int rx_buf_len, MqttUnsubscribe *unsubs /* [MQTT-3.10.3-2] The Payload of an UNSUBSCRIBE packet MUST * contain at least one Topic Filter. v5 section 3.10.3 carries the same * minimum-cardinality requirement. */ - if (unsubscribe->topic_count == 0) { - return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + if (tmp >= 0 && unsubscribe->topic_count == 0) { + tmp = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } + if (tmp < 0) { + #ifdef WOLFMQTT_V5 + /* Free any v5 properties decoded above so a malformed topic + * filter cannot leak the property list; the broker caller only + * frees props on the success path. */ + MqttProps_Free(unsubscribe->props); + unsubscribe->props = NULL; + #endif + return tmp; } } @@ -2888,8 +2917,10 @@ int MqttDecode_UnsubscribeAck(byte *rx_buf, int rx_buf_len, } } - /* Reason codes are stored in the payload */ + /* Reason codes are stored in the payload, one per topic filter */ unsubscribe_ack->reason_codes = rx_payload; + unsubscribe_ack->reason_code_count = + (word16)((rx_buf + header_len + remain_len) - rx_payload); } #endif } diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index ef3f6dd03..1e193719e 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -1562,6 +1562,61 @@ TEST(broker_unhandled_packet_type_closes) MqttBroker_Free(&broker); } +/* [MQTT-3.1.0-1]: a client's first packet MUST be CONNECT. The broker's + * pre-dispatch guard closes any client that sends another packet type + * first. A PUBLISH from a never-connected client must be dropped and the + * client closed - it must NOT fan out to subscribers. A deletion of the + * guard would let BrokerHandle_Publish run on the unauthenticated client, + * so the subscriber receiving zero PUBLISH packets is the load-bearing + * assertion. */ +TEST(broker_publish_before_connect_closes) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + /* Subscriber: CONNECT then SUBSCRIBE to "t" (qos 0). */ + static const byte sub_connect[] = { + 0x10, 0x0D, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x01, 'S' + }; + static const byte sub_subscribe[] = { + 0x82, 0x06, + 0x00, 0x01, /* packet_id */ + 0x00, 0x01, 't', + 0x00 + }; + /* Attacker: PUBLISH "t"/"x" as the very first packet, no CONNECT. */ + static const byte pub_no_connect[] = { + 0x30, 0x04, + 0x00, 0x01, 't', + 'x' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(2); + mock_client_input_append(0, sub_connect, sizeof(sub_connect)); + mock_client_input_append(0, sub_subscribe, sizeof(sub_subscribe)); + mock_client_input_append(1, pub_no_connect, sizeof(pub_no_connect)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + /* Attacker connection closed for violating [MQTT-3.1.0-1]. */ + ASSERT_TRUE(g_clients[1].closed); + /* Subscriber must have received no fan-out from the pre-CONNECT PUBLISH. */ + ASSERT_EQ(0, count_packets_of_type(g_clients[0].out_buf, + g_clients[0].out_len, MQTT_PACKET_TYPE_PUBLISH)); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} + /* [MQTT-2.3.1-1] / [MQTT-4.13]: a SUBSCRIBE packet with Packet * Identifier = 0 is malformed and the broker MUST close the connection. * MqttDecode_Subscribe returns MQTT_CODE_ERROR_PACKET_ID; this test @@ -2446,6 +2501,7 @@ int main(int argc, char** argv) #endif RUN_TEST(disconnect_invalid_fixed_header_flags_fires_will); RUN_TEST(broker_unhandled_packet_type_closes); + RUN_TEST(broker_publish_before_connect_closes); RUN_TEST(broker_subscribe_packet_id_zero_closes); RUN_TEST(connack_session_present_set_on_resumed_session); RUN_TEST(connack_session_present_set_on_takeover); diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index f873e94ea..bdc6153e0 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -3052,6 +3052,34 @@ TEST(decode_subscribe_v5_empty_payload_rejected) rc = MqttDecode_Subscribe(rx_buf, (int)sizeof(rx_buf), &sub); ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); } + +/* A v5 SUBSCRIBE whose Properties block decodes cleanly but whose topic + * filter is malformed must not leak the decoded property list. The decoder + * fails on the bad filter after allocating the User Property; without the + * cleanup the broker caller returns before freeing props. Catches the + * structural invariant: sub.props == NULL on error. */ +TEST(decode_subscribe_v5_props_freed_on_bad_filter) +{ + byte rx_buf[] = { + 0x82, 0x0F, /* SUBSCRIBE, remain_len = 15 */ + 0x00, 0x01, /* packet_id */ + 0x07, /* props_len VBI = 7 */ + 0x26, 0x00, 0x01, 'k', 0x00, 0x01, 'v', /* User Property k=v */ + 0x00, 0x02, 'a', '#', /* bad filter "a#" */ + 0x00 /* options */ + }; + MqttSubscribe sub; + MqttTopic topic_arr[1]; + int rc; + + XMEMSET(&sub, 0, sizeof(sub)); + XMEMSET(topic_arr, 0, sizeof(topic_arr)); + sub.topics = topic_arr; + sub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Subscribe(rx_buf, (int)sizeof(rx_buf), &sub); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(sub.props); +} #endif /* WOLFMQTT_V5 */ /* [MQTT-4.7.3-1] / [MQTT-4.7.1-2] / [MQTT-4.7.1-3] Topic Filter syntax @@ -3368,6 +3396,31 @@ TEST(decode_unsubscribe_v5_empty_payload_rejected) rc = MqttDecode_Unsubscribe(rx_buf, (int)sizeof(rx_buf), &unsub); ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); } + +/* A v5 UNSUBSCRIBE whose Properties block decodes cleanly but whose topic + * filter is malformed must not leak the decoded property list. Mirrors the + * SUBSCRIBE case: catches the invariant unsub.props == NULL on error. */ +TEST(decode_unsubscribe_v5_props_freed_on_bad_filter) +{ + byte rx_buf[] = { + 0xA2, 0x0E, /* UNSUBSCRIBE, remain_len = 14 */ + 0x00, 0x01, /* packet_id */ + 0x07, /* props_len VBI = 7 */ + 0x26, 0x00, 0x01, 'k', 0x00, 0x01, 'v', /* User Property k=v */ + 0x00, 0x02, 'a', '#' /* bad filter "a#" */ + }; + MqttUnsubscribe unsub; + MqttTopic topic_arr[1]; + int rc; + + XMEMSET(&unsub, 0, sizeof(unsub)); + XMEMSET(topic_arr, 0, sizeof(topic_arr)); + unsub.topics = topic_arr; + unsub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Unsubscribe(rx_buf, (int)sizeof(rx_buf), &unsub); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(unsub.props); +} #endif /* WOLFMQTT_V5 */ /* UNSUBSCRIBE shares the same Topic Filter syntax rules as SUBSCRIBE @@ -3930,6 +3983,32 @@ TEST(decode_unsuback_malformed_remain_len_one) ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); } +#ifdef WOLFMQTT_V5 +/* A v5 UNSUBACK carries one reason code per topic filter after the + * properties block. The decoder must expose the count and bytes so the + * client can detect a rejection (high-bit code), mirroring SUBSCRIBE. */ +TEST(decode_unsuback_v5_reason_codes) +{ + byte buf[] = { + 0xB0, 0x05, /* UNSUBACK, remain_len = 5 */ + 0x00, 0x01, /* packet_id */ + 0x00, /* props_len = 0 */ + 0x00, 0x87 /* success, NOT_AUTHORIZED */ + }; + MqttUnsubscribeAck ack; + int rc; + + XMEMSET(&ack, 0, sizeof(ack)); + ack.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_UnsubscribeAck(buf, (int)sizeof(buf), &ack); + ASSERT_TRUE(rc > 0); + ASSERT_EQ(2, ack.reason_code_count); + ASSERT_TRUE(ack.reason_codes != NULL); + ASSERT_EQ(0x00, ack.reason_codes[0]); + ASSERT_EQ(0x87, ack.reason_codes[1]); +} +#endif /* WOLFMQTT_V5 */ + /* ============================================================================ * MqttDecode_Ping (PINGRESP) and MqttDecode_Disconnect length validation * @@ -4710,6 +4789,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_subscribe_empty_payload_rejected); #ifdef WOLFMQTT_V5 RUN_TEST(decode_subscribe_v5_empty_payload_rejected); + RUN_TEST(decode_subscribe_v5_props_freed_on_bad_filter); #endif #ifdef WOLFMQTT_V5 RUN_TEST(decode_subscribe_v5_options_byte_qos_extracted); @@ -4726,6 +4806,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_unsubscribe_bad_plus_placement_rejected); #ifdef WOLFMQTT_V5 RUN_TEST(decode_unsubscribe_v5_empty_payload_rejected); + RUN_TEST(decode_unsubscribe_v5_props_freed_on_bad_filter); #endif #endif @@ -4772,6 +4853,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_unsuback_valid); RUN_TEST(decode_unsuback_malformed_remain_len_zero); RUN_TEST(decode_unsuback_malformed_remain_len_one); + RUN_TEST(decode_unsuback_v5_reason_codes); /* MqttDecode_Ping (PINGRESP) length validation */ RUN_TEST(decode_pingresp_valid); diff --git a/wolfmqtt/mqtt_types.h b/wolfmqtt/mqtt_types.h index 1ce8a94b5..cfcc4a73e 100644 --- a/wolfmqtt/mqtt_types.h +++ b/wolfmqtt/mqtt_types.h @@ -211,6 +211,10 @@ enum MqttPacketResponseCodes { see each MqttTopic.return_code for the per-filter result. */ + MQTT_CODE_ERROR_UNSUBSCRIBE_REJECTED = -20, /* Broker rejected one or more + topic filters in an + UNSUBSCRIBE; see each reason + code in MqttUnsubscribeAck. */ MQTT_CODE_CONTINUE = -101, MQTT_CODE_STDIN_WAKE = -102, From 60d20f6f76489f885e9f5cbe9cf2fc9b1176da39 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 13:45:36 -0700 Subject: [PATCH 02/31] F-4244 F-4307 - Defer WebSocket close during publish fan-out and snapshot sub iterator --- src/mqtt_broker.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index d917a68c5..fe3ad5b0e 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -1105,6 +1105,7 @@ static int BrokerWsNetWrite(void* context, const byte* buf, int buf_len, BrokerClient* bc = (BrokerClient*)context; BrokerWsCtx* ws; int attempts = 0; + int prev_processing; (void)timeout_ms; @@ -1132,12 +1133,19 @@ static int BrokerWsNetWrite(void* context, const byte* buf, int buf_len, XMEMCPY(ws->tx_pending + LWS_PRE, buf, buf_len); ws->tx_len = (size_t)buf_len; - /* Request writable callback and service until data is flushed */ + /* Request writable callback and service until data is flushed. Mark this + * context busy across the spin so a peer-initiated LWS_CALLBACK_CLOSED for + * this wsi (e.g. a subscriber whose peer closes during publish fan-out) + * takes the deferred-remove path instead of freeing ws and bc out from + * under this function. Mirrors the publisher guard in BrokerClient_Process. */ lws_callback_on_writable((struct lws*)ws->wsi); + prev_processing = ws->processing; + ws->processing = 1; while (ws->tx_pending != NULL && ws->status > 0 && attempts < 100) { lws_service(lws_get_context((struct lws*)ws->wsi), 0); attempts++; } + ws->processing = prev_processing; if (ws->tx_pending != NULL) { /* Data was not flushed - connection may be in bad state */ @@ -4935,6 +4943,7 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, int i; #else BrokerSub* sub = broker->subs; + BrokerSub* next_sub = NULL; #endif /* Fan out to matching subscribers */ #ifdef WOLFMQTT_STATIC_MEMORY @@ -4943,6 +4952,11 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, if (!sub->in_use) continue; #else while (sub) { + /* Snapshot the successor before any MqttPacket_Write: a fan-out + * write can drive an lws_service spin that frees this client's + * BrokerSub nodes re-entrantly (LWS_CALLBACK_CLOSED), so reading + * sub->next afterwards would dereference a freed node. */ + next_sub = sub->next; #endif if (sub->client != NULL && sub->client->protocol_level != 0 && @@ -5056,7 +5070,7 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, } } } - sub = sub->next; + sub = next_sub; #endif } } From 57cfd36c2c6e3dbef1eabbcf25da4c857e70bf48 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 13:50:53 -0700 Subject: [PATCH 03/31] F-4246 F-4247 - Reject v5 empty topic without alias and zero subscription identifier --- src/mqtt_packet.c | 25 ++++++++++++++++++++++ tests/test_mqtt_packet.c | 45 ++++++++++++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 672a593b7..40c155061 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -964,6 +964,12 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, buf += tmp; total += tmp; prop_len -= (word32)tmp; + /* [MQTT-3.8.2.1.2] A Subscription Identifier of 0 is + * reserved and a Protocol Error. */ + if (cur_prop->type == MQTT_PROP_SUBSCRIPTION_ID && + cur_prop->data_int == 0) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } break; } case MQTT_DATA_TYPE_BINARY: @@ -1990,6 +1996,8 @@ int MqttDecode_Publish(byte *rx_buf, int rx_buf_len, MqttPublish *publish) if (publish->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { word32 props_len = 0; int tmp; + MqttProp* prop_iter; + byte has_topic_alias = 0; /* Decode Length of Properties */ if (rx_buf_len < (rx_payload - rx_buf)) { @@ -2015,6 +2023,23 @@ int MqttDecode_Publish(byte *rx_buf, int rx_buf_len, MqttPublish *publish) else { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); } + + /* [MQTT-3.3.2-8] v5 section 3.3.2.3.4: a zero-length Topic Name is + * only valid when a Topic Alias property supplies the topic. */ + if (publish->topic_name_len == 0) { + for (prop_iter = publish->props; prop_iter != NULL; + prop_iter = prop_iter->next) { + if (prop_iter->type == MQTT_PROP_TOPIC_ALIAS) { + has_topic_alias = 1; + break; + } + } + if (!has_topic_alias) { + MqttProps_Free(publish->props); + publish->props = NULL; + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } + } } #endif diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index bdc6153e0..f7e837e38 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -1209,12 +1209,12 @@ TEST(decode_publish_topic_contains_u0000_rejected) } #ifdef WOLFMQTT_V5 -/* MQTT v5 section 3.3.2.3.4: a zero-length Topic Name is permitted (paired - * with a Topic Alias property at the application layer). Wire shape: - * PUBLISH | QoS 0, remain=4, topic_len=0, props_len=0, payload "x". */ -TEST(decode_publish_v5_empty_topic_accepted) +/* MQTT v5 section 3.3.2.3.4: a zero-length Topic Name is permitted only when + * paired with a Topic Alias property. Wire: PUBLISH QoS 0, remain=7, + * topic_len=0, props_len=3, TOPIC_ALIAS(35)=1, payload "x". */ +TEST(decode_publish_v5_empty_topic_with_alias_accepted) { - byte buf[] = { 0x30, 0x04, 0x00, 0x00, 0x00, 'x' }; + byte buf[] = { 0x30, 0x07, 0x00, 0x00, 0x03, 0x23, 0x00, 0x01, 'x' }; MqttPublish pub; int rc; @@ -1223,6 +1223,37 @@ TEST(decode_publish_v5_empty_topic_accepted) rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub); ASSERT_TRUE(rc > 0); ASSERT_EQ(0, pub.topic_name_len); + MqttProps_Free(pub.props); +} + +/* [MQTT-3.3.2-8] A zero-length Topic Name with no Topic Alias property is a + * Protocol Error. Wire: PUBLISH QoS 0, remain=4, topic_len=0, props_len=0. */ +TEST(decode_publish_v5_empty_topic_no_alias_rejected) +{ + byte buf[] = { 0x30, 0x04, 0x00, 0x00, 0x00, 'x' }; + MqttPublish pub; + int rc; + + XMEMSET(&pub, 0, sizeof(pub)); + pub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(pub.props); +} + +/* [MQTT-3.8.2.1.2] A Subscription Identifier of 0 is reserved and a Protocol + * Error. Wire: PUBLISH QoS 0, topic "t", props_len=2, SUBSCRIPTION_ID(11)=0. */ +TEST(decode_publish_v5_subscription_id_zero_rejected) +{ + byte buf[] = { 0x30, 0x07, 0x00, 0x01, 't', 0x02, 0x0B, 0x00, 'x' }; + MqttPublish pub; + int rc; + + XMEMSET(&pub, 0, sizeof(pub)); + pub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(pub.props); } #endif /* WOLFMQTT_V5 */ @@ -4680,7 +4711,9 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_publish_wildcard_plus_topic_rejected); RUN_TEST(decode_publish_topic_contains_u0000_rejected); #ifdef WOLFMQTT_V5 - RUN_TEST(decode_publish_v5_empty_topic_accepted); + RUN_TEST(decode_publish_v5_empty_topic_with_alias_accepted); + RUN_TEST(decode_publish_v5_empty_topic_no_alias_rejected); + RUN_TEST(decode_publish_v5_subscription_id_zero_rejected); #endif RUN_TEST(decode_publish_qos1_packet_id_zero_rejected); RUN_TEST(decode_publish_qos2_packet_id_zero_rejected); From 1ef1d06f36f4ec9f990411a3a54c82950f9de454 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 13:55:07 -0700 Subject: [PATCH 04/31] F-4245 F-4249 - Reject overlong static topics and clamp v5 will delay interval --- src/mqtt_broker.c | 27 ++++++++++++++++++++++++--- wolfmqtt/mqtt_broker.h | 6 ++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index fe3ad5b0e..e7616018c 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -3685,7 +3685,13 @@ static void BrokerClient_PublishWill(MqttBroker* broker, BrokerClient* bc) BrokerClient_ClearWill(bc); return; /* will deferred, not published now */ } - /* If add failed (out of slots), publish immediately as fallback */ + /* Out of pending-will slots: fall back to immediate publication, but + * surface it - a silent fallback lets slot exhaustion erase the Will + * Delay grace window invisibly to the operator. */ + WBLOG_ERR(broker, + "broker: pending-will pool full, publishing LWT immediately " + "(delay=%u lost) sock=%d", (unsigned)bc->will_delay_sec, + (int)bc->sock); } WBLOG_DBG(broker, "broker: LWT publish sock=%d topic=%s len=%u", @@ -4336,7 +4342,15 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, MqttProp* prop = BrokerProps_Find(mc.lwt_msg->props, MQTT_PROP_WILL_DELAY_INTERVAL); if (prop != NULL) { - bc->will_delay_sec = prop->data_int; + /* Clamp to a sane maximum so a client advertising a huge + * delay (e.g. UINT32_MAX) cannot monopolize a pending-will + * slot indefinitely. */ + if (prop->data_int > BROKER_MAX_WILL_DELAY_SEC) { + bc->will_delay_sec = BROKER_MAX_WILL_DELAY_SEC; + } + else { + bc->will_delay_sec = prop->data_int; + } } } #endif @@ -4880,7 +4894,14 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, #ifdef WOLFMQTT_STATIC_MEMORY word16 tlen = pub.topic_name_len; if (tlen >= BROKER_MAX_TOPIC_LEN) { - tlen = BROKER_MAX_TOPIC_LEN - 1; + /* Reject rather than truncate: a truncated topic can match a + * different subscriber filter than the wire topic (filter/auth + * bypass) and collide retained-message keys. */ + WBLOG_ERR(broker, + "broker: PUBLISH topic too long len=%u max=%d sock=%d", + (unsigned)tlen, BROKER_MAX_TOPIC_LEN, (int)bc->sock); + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + goto publish_cleanup; } XMEMCPY(topic_buf, pub.topic_name, tlen); topic_buf[tlen] = '\0'; diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index 316e1427e..63d3b233f 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -120,6 +120,12 @@ #ifndef BROKER_MAX_PENDING_WILLS #define BROKER_MAX_PENDING_WILLS 4 #endif +/* Upper bound (seconds) the broker accepts for a v5 Will Delay Interval. + * Caps how long a deferred-will slot can be monopolized so a few clients + * advertising near-UINT32_MAX delays cannot permanently exhaust the pool. */ +#ifndef BROKER_MAX_WILL_DELAY_SEC + #define BROKER_MAX_WILL_DELAY_SEC 3600 +#endif /* Maximum concurrent inbound QoS 2 packet IDs awaiting PUBREL per client. * Used to dedup duplicate PUBLISHes per [MQTT-4.3.3] (Method B). 16 covers * any reasonable client; a misbehaving client that exceeds this gets a From 2581a2cd2c1beddef4102828964a3df79978b86e Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 14:11:00 -0700 Subject: [PATCH 05/31] F-4304 F-4654 F-4655 F-4722 F-4723 F-4727 F-4729 F-4776 F-4933 - Sanitize peer strings in broker log sinks --- src/mqtt_broker.c | 164 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 126 insertions(+), 38 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index e7616018c..412f464d8 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -116,6 +116,72 @@ static void MqttBroker_ForceZero(void* mem, word32 len) #define WBLOG_DBG(b, ...) WBLOG(b, BROKER_LOG_DEBUG, __VA_ARGS__) #endif +#ifndef WOLFMQTT_BROKER_NO_LOG +#define BROKER_LOG_SAN_SZ 128 /* per-string scratch size */ +#define BROKER_LOG_SAN_POOL 4 /* distinct buffers per log statement */ + +/* Sanitize a peer-controlled string (topic, filter, client_id, cert CN, ...) + * before it reaches a PRINTF log sink (CWE-117). Control bytes (< 0x20 and + * DEL 0x7f) become printable escapes so a remote peer cannot inject forged log + * lines (CR/LF) or hijack the operator terminal (ANSI ESC). The result is + * returned from a small rotating pool of static buffers so several sanitized + * arguments can appear in one log statement; the broker log path is single + * threaded (as is PRINTF itself). Output is NUL-terminated and truncated to + * fit. NULL src yields "(null)". */ +static const char* BrokerLog_Sanitize(const char* src) +{ + static const char hex_digits[] = "0123456789abcdef"; + static char pool[BROKER_LOG_SAN_POOL][BROKER_LOG_SAN_SZ]; + static int pool_idx = 0; + char* dst = pool[pool_idx]; + word32 di = 0; + + pool_idx = (pool_idx + 1) % BROKER_LOG_SAN_POOL; + + if (src == NULL) { + src = "(null)"; + } + + while (*src != '\0') { + byte c = (byte)*src++; + char rep[4]; + word32 repLen = 0; + word32 j; + + switch (c) { + case '\r': rep[0] = '\\'; rep[1] = 'r'; repLen = 2; break; + case '\n': rep[0] = '\\'; rep[1] = 'n'; repLen = 2; break; + case '\t': rep[0] = '\\'; rep[1] = 't'; repLen = 2; break; + case '\v': rep[0] = '\\'; rep[1] = 'v'; repLen = 2; break; + case 0x1b: rep[0] = '\\'; rep[1] = 'e'; repLen = 2; break; + default: + if (c < 0x20 || c == 0x7f) { + rep[0] = '\\'; + rep[1] = 'x'; + rep[2] = hex_digits[(c >> 4) & 0x0f]; + rep[3] = hex_digits[c & 0x0f]; + repLen = 4; + } + else { + rep[0] = (char)c; + repLen = 1; + } + break; + } + + if (di + repLen + 1 > BROKER_LOG_SAN_SZ) { + break; + } + for (j = 0; j < repLen; j++) { + dst[di++] = rep[j]; + } + } + + dst[di] = '\0'; + return dst; +} +#endif /* !WOLFMQTT_BROKER_NO_LOG */ + /* Buffer size accessors - unify static/dynamic code paths */ #ifdef WOLFMQTT_STATIC_MEMORY #define BROKER_CLIENT_TX_SZ(bc) BROKER_TX_BUF_SZ @@ -1687,7 +1753,7 @@ static void BrokerClient_DrainOutQueue(BrokerClient* bc) if (enc_rc <= 0) { WBLOG_ERR(bc->broker, "broker: drain encode failed sock=%d topic=%s rc=%d", - (int)bc->sock, cur->topic, enc_rc); + (int)bc->sock, BrokerLog_Sanitize(cur->topic), enc_rc); /* Drop just this entry and continue. Encoding failure for * a single message is not fatal to the connection. */ if (prev == NULL) { @@ -1733,13 +1799,13 @@ static void BrokerClient_DrainOutQueue(BrokerClient* bc) * stop the drain here. */ WBLOG_ERR(bc->broker, "broker: drain write failed sock=%d topic=%s rc=%d", - (int)bc->sock, cur->topic, wr_rc); + (int)bc->sock, BrokerLog_Sanitize(cur->topic), wr_rc); return; } } WBLOG_DBG(bc->broker, "broker: drain send sock=%d topic=%s qos=%d packet_id=%u dup=%d", - (int)bc->sock, cur->topic, (int)cur->qos, + (int)bc->sock, BrokerLog_Sanitize(cur->topic), (int)cur->qos, (unsigned)cur->packet_id, (int)cur->retransmit_dup); cur->retransmit_dup = 0; @@ -2223,8 +2289,8 @@ static int BrokerOrphan_EvictOldest(MqttBroker* broker) } WBLOG_INFO(broker, "broker: evicting oldest orphan client_id=%s (cap reached)", - BROKER_STR_VALID(oldest->client_id) ? oldest->client_id - : "(null)"); + BrokerLog_Sanitize(BROKER_STR_VALID(oldest->client_id) + ? oldest->client_id : "(null)")); BrokerOrphan_DropFull(broker, oldest); return 1; } @@ -2351,7 +2417,7 @@ static BrokerOrphanSession* BrokerOrphan_Take(MqttBroker* broker, broker->orphan_session_count++; WBLOG_INFO(broker, "broker: orphan session created client_id=%s queued=%d", - o->client_id, o->out_q_count); + BrokerLog_Sanitize(o->client_id), o->out_q_count); #ifdef WOLFMQTT_BROKER_PERSIST /* Re-persist the session record stamped with the orphan time so the v5 @@ -2431,12 +2497,12 @@ static int BrokerOrphan_Reclaim(MqttBroker* broker, BrokerClient* new_bc) if (retx > 0) { WBLOG_INFO(broker, "broker: orphan reclaim queued retransmit=%d client_id=%s", - retx, new_bc->client_id); + retx, BrokerLog_Sanitize(new_bc->client_id)); } } WBLOG_INFO(broker, "broker: orphan reclaimed client_id=%s queued=%d", - new_bc->client_id, new_bc->out_q_count); + BrokerLog_Sanitize(new_bc->client_id), new_bc->out_q_count); #ifdef WOLFMQTT_BROKER_PERSIST /* The reclaimed queue is now in a LIVE BrokerClient. Persisted * records for this client_id are no longer authoritative - the @@ -2497,7 +2563,8 @@ static void BrokerOrphan_Enqueue(MqttBroker* broker, BrokerOrphanSession* o, if (e == NULL) { WBLOG_ERR(broker, "broker: orphan enqueue alloc failed client_id=%s", - BROKER_STR_VALID(o->client_id) ? o->client_id : "(null)"); + BrokerLog_Sanitize( + BROKER_STR_VALID(o->client_id) ? o->client_id : "(null)")); return; } e->qos = qos; @@ -2520,8 +2587,9 @@ static void BrokerOrphan_Enqueue(MqttBroker* broker, BrokerOrphanSession* o, #endif WBLOG_DBG(broker, "broker: orphan enqueue client_id=%s topic=%s qos=%d count=%d", - BROKER_STR_VALID(o->client_id) ? o->client_id : "(null)", - topic, (int)qos, o->out_q_count); + BrokerLog_Sanitize( + BROKER_STR_VALID(o->client_id) ? o->client_id : "(null)"), + BrokerLog_Sanitize(topic), (int)qos, o->out_q_count); } /* Free every orphan (used by MqttBroker_Free and by wipe paths). */ @@ -2593,7 +2661,8 @@ static void BrokerSubs_OrphanClient(MqttBroker* broker, BrokerClient* bc) if (BrokerOrphan_Take(broker, bc) == NULL) { WBLOG_ERR(broker, "broker: orphan take failed client_id=%s - removing %d subs", - BROKER_STR_VALID(bc->client_id) ? bc->client_id : "(null)", + BrokerLog_Sanitize( + BROKER_STR_VALID(bc->client_id) ? bc->client_id : "(null)"), count); BrokerSubs_RemoveClient(broker, bc); return; @@ -2620,7 +2689,8 @@ static void BrokerSubs_OrphanClient(MqttBroker* broker, BrokerClient* bc) #endif WBLOG_INFO(broker, "broker: orphaned %d subs for client_id=%s (session persist)", - count, BROKER_STR_VALID(bc->client_id) ? bc->client_id : "(null)"); + count, BrokerLog_Sanitize( + BROKER_STR_VALID(bc->client_id) ? bc->client_id : "(null)")); } static void BrokerSubs_RemoveClient(MqttBroker* broker, BrokerClient* bc) @@ -2698,7 +2768,7 @@ static int BrokerSubs_Add(MqttBroker* broker, BrokerClient* bc, XMEMCMP(broker->subs[i].filter, filter, filter_len) == 0) { broker->subs[i].qos = qos; WBLOG_INFO(broker, "broker: sub update sock=%d filter=%s qos=%d", - (int)bc->sock, broker->subs[i].filter, qos); + (int)bc->sock, BrokerLog_Sanitize(broker->subs[i].filter), qos); return MQTT_CODE_SUCCESS; } } @@ -2709,7 +2779,7 @@ static int BrokerSubs_Add(MqttBroker* broker, BrokerClient* bc, XMEMCMP(cur->filter, filter, filter_len) == 0) { cur->qos = qos; WBLOG_INFO(broker, "broker: sub update sock=%d filter=%s qos=%d", - (int)bc->sock, cur->filter, qos); + (int)bc->sock, BrokerLog_Sanitize(cur->filter), qos); return MQTT_CODE_SUCCESS; } cur = cur->next; @@ -2783,7 +2853,7 @@ static int BrokerSubs_Add(MqttBroker* broker, BrokerClient* bc, } #endif WBLOG_INFO(broker, "broker: sub add sock=%d filter=%s qos=%d", - (int)bc->sock, sub->filter, qos); + (int)bc->sock, BrokerLog_Sanitize(sub->filter), qos); } return rc; } @@ -2806,7 +2876,7 @@ static void BrokerSubs_Remove(MqttBroker* broker, BrokerClient* bc, (word16)XSTRLEN(s->filter) == filter_len && XMEMCMP(s->filter, filter, filter_len) == 0) { WBLOG_INFO(broker, "broker: sub remove sock=%d filter=%s", - (int)bc->sock, s->filter); + (int)bc->sock, BrokerLog_Sanitize(s->filter)); XMEMSET(s, 0, sizeof(BrokerSub)); return; } @@ -2826,7 +2896,7 @@ static void BrokerSubs_Remove(MqttBroker* broker, BrokerClient* bc, broker->subs = next; } WBLOG_INFO(broker, "broker: sub remove sock=%d filter=%s", - (int)bc->sock, cur->filter); + (int)bc->sock, BrokerLog_Sanitize(cur->filter)); WOLFMQTT_FREE(cur->filter); if (cur->client_id) { WOLFMQTT_FREE(cur->client_id); @@ -3017,7 +3087,7 @@ static int BrokerSubs_ReassociateClient(MqttBroker* broker, #endif if (count > 0) { WBLOG_INFO(broker, "broker: reassociated %d subs for client_id=%s", - count, client_id); + count, BrokerLog_Sanitize(client_id)); } return count; } @@ -3153,7 +3223,8 @@ static int BrokerRetained_Store(MqttBroker* broker, const char* topic, msg->qos = qos; WBLOG_DBG(broker, "broker: retained store topic=%s len=%u qos=%d " "expiry=%u", - topic, (unsigned)payload_len, (int)qos, (unsigned)expiry_sec); + BrokerLog_Sanitize(topic), (unsigned)payload_len, (int)qos, + (unsigned)expiry_sec); #ifdef WOLFMQTT_BROKER_PERSIST (void)BrokerPersist_PutRetained(broker, msg); #endif @@ -3178,7 +3249,8 @@ static void BrokerRetained_Delete(MqttBroker* broker, const char* topic) for (i = 0; i < BROKER_MAX_RETAINED; i++) { if (broker->retained[i].in_use && XSTRCMP(broker->retained[i].topic, topic) == 0) { - WBLOG_DBG(broker, "broker: retained delete topic=%s", topic); + WBLOG_DBG(broker, "broker: retained delete topic=%s", + BrokerLog_Sanitize(topic)); XMEMSET(&broker->retained[i], 0, sizeof(BrokerRetainedMsg)); found = 1; break; @@ -3189,7 +3261,8 @@ static void BrokerRetained_Delete(MqttBroker* broker, const char* topic) while (cur) { BrokerRetainedMsg* next = cur->next; if (cur->topic != NULL && XSTRCMP(cur->topic, topic) == 0) { - WBLOG_DBG(broker, "broker: retained delete topic=%s", topic); + WBLOG_DBG(broker, "broker: retained delete topic=%s", + BrokerLog_Sanitize(topic)); if (prev) { prev->next = next; } @@ -3393,7 +3466,8 @@ static int BrokerPendingWill_Add(MqttBroker* broker, BrokerClient* bc) pw->retain = bc->will_retain; pw->publish_time = now + (WOLFMQTT_BROKER_TIME_T)bc->will_delay_sec; WBLOG_DBG(broker, "broker: will deferred sock=%d client_id=%s delay=%u", - (int)bc->sock, bc->client_id, (unsigned)bc->will_delay_sec); + (int)bc->sock, BrokerLog_Sanitize(bc->client_id), + (unsigned)bc->will_delay_sec); } return rc; } @@ -3416,7 +3490,8 @@ static void BrokerPendingWill_Cancel(MqttBroker* broker, for (i = 0; i < BROKER_MAX_PENDING_WILLS; i++) { if (broker->pending_wills[i].in_use && XSTRCMP(broker->pending_wills[i].client_id, client_id) == 0) { - WBLOG_DBG(broker, "broker: will cancelled client_id=%s", client_id); + WBLOG_DBG(broker, "broker: will cancelled client_id=%s", + BrokerLog_Sanitize(client_id)); XMEMSET(&broker->pending_wills[i], 0, sizeof(BrokerPendingWill)); return; @@ -3428,7 +3503,8 @@ static void BrokerPendingWill_Cancel(MqttBroker* broker, BrokerPendingWill* next = pw->next; if (pw->client_id != NULL && XSTRCMP(pw->client_id, client_id) == 0) { - WBLOG_DBG(broker, "broker: will cancelled client_id=%s", client_id); + WBLOG_DBG(broker, "broker: will cancelled client_id=%s", + BrokerLog_Sanitize(client_id)); if (prev) { prev->next = next; } @@ -3512,7 +3588,8 @@ static int BrokerPendingWill_Process(MqttBroker* broker) } if (now >= pw->publish_time) { WBLOG_DBG(broker, "broker: LWT deferred publish client_id=%s topic=%s " - "len=%u", pw->client_id, pw->topic, + "len=%u", BrokerLog_Sanitize(pw->client_id), + BrokerLog_Sanitize(pw->topic), (unsigned)pw->payload_len); BrokerClient_PublishWillImmediate(broker, pw->topic, pw->payload, pw->payload_len, pw->qos, pw->retain); @@ -3526,7 +3603,8 @@ static int BrokerPendingWill_Process(MqttBroker* broker) BrokerPendingWill* next = pw->next; if (now >= pw->publish_time) { WBLOG_DBG(broker, "broker: LWT deferred publish client_id=%s topic=%s " - "len=%u", pw->client_id, pw->topic, + "len=%u", BrokerLog_Sanitize(pw->client_id), + BrokerLog_Sanitize(pw->topic), (unsigned)pw->payload_len); BrokerClient_PublishWillImmediate(broker, pw->topic, pw->payload, pw->payload_len, pw->qos, pw->retain); @@ -3585,7 +3663,8 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, /* Skip expired messages */ if (rm->expiry_sec > 0 && (now - rm->store_time) >= rm->expiry_sec) { - WBLOG_DBG(broker, "broker: retained expired topic=%s", rm->topic); + WBLOG_DBG(broker, "broker: retained expired topic=%s", + BrokerLog_Sanitize(rm->topic)); XMEMSET(rm, 0, sizeof(BrokerRetainedMsg)); continue; } @@ -3610,7 +3689,8 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, BROKER_CLIENT_TX_SZ(bc), &out_pub, 0); if (enc_rc > 0) { WBLOG_DBG(broker, "broker: retained deliver sock=%d topic=%s " - "len=%u qos=%d", (int)bc->sock, rm->topic, + "len=%u qos=%d", (int)bc->sock, + BrokerLog_Sanitize(rm->topic), (unsigned)rm->payload_len, (int)eff_qos); (void)MqttPacket_Write(&bc->client, bc->tx_buf, enc_rc); } @@ -3623,7 +3703,8 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, /* Skip and remove expired messages */ if (rm->expiry_sec > 0 && (now - rm->store_time) >= rm->expiry_sec) { - WBLOG_DBG(broker, "broker: retained expired topic=%s", rm->topic); + WBLOG_DBG(broker, "broker: retained expired topic=%s", + BrokerLog_Sanitize(rm->topic)); if (rm_prev) { rm_prev->next = rm_next; } @@ -3657,7 +3738,8 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, BROKER_CLIENT_TX_SZ(bc), &out_pub, 0); if (enc_rc > 0) { WBLOG_DBG(broker, "broker: retained deliver sock=%d topic=%s " - "len=%u qos=%d", (int)bc->sock, rm->topic, + "len=%u qos=%d", (int)bc->sock, + BrokerLog_Sanitize(rm->topic), (unsigned)rm->payload_len, (int)eff_qos); (void)MqttPacket_Write(&bc->client, bc->tx_buf, enc_rc); } @@ -3695,7 +3777,8 @@ static void BrokerClient_PublishWill(MqttBroker* broker, BrokerClient* bc) } WBLOG_DBG(broker, "broker: LWT publish sock=%d topic=%s len=%u", - (int)bc->sock, bc->will_topic, (unsigned)bc->will_payload_len); + (int)bc->sock, BrokerLog_Sanitize(bc->will_topic), + (unsigned)bc->will_payload_len); BrokerClient_PublishWillImmediate(broker, bc->will_topic, bc->will_payload, bc->will_payload_len, bc->will_qos, @@ -4210,7 +4293,8 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, WBLOG_INFO(broker, "broker: CONNECT proto=%u clean=%d will=%d client_id=%s", mc.protocol_level, mc.clean_session, mc.enable_lwt, - BROKER_STR_VALID(bc->client_id) ? bc->client_id : "(null)"); + BrokerLog_Sanitize( + BROKER_STR_VALID(bc->client_id) ? bc->client_id : "(null)")); /* Client ID uniqueness and clean session handling */ bc->clean_session = mc.clean_session; @@ -4224,7 +4308,8 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, old = BrokerClient_FindByClientId(broker, bc->client_id, bc); if (old != NULL) { WBLOG_INFO(broker, "broker: duplicate client_id=%s, disconnecting " - "old sock=%d", bc->client_id, (int)old->sock); + "old sock=%d", BrokerLog_Sanitize(bc->client_id), + (int)old->sock); /* Publish old client's will on takeover */ #ifdef WOLFMQTT_V5 if (old->protocol_level < MQTT_CONNECT_PROTOCOL_LEVEL_5) { @@ -4356,7 +4441,7 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, #endif bc->has_will = 1; WBLOG_DBG(broker, "broker: LWT stored sock=%d topic=%s qos=%d retain=%d " - "len=%u delay=%u", (int)bc->sock, bc->will_topic, + "len=%u delay=%u", (int)bc->sock, BrokerLog_Sanitize(bc->will_topic), bc->will_qos, bc->will_retain, (unsigned)bc->will_payload_len, (unsigned)bc->will_delay_sec); @@ -5016,7 +5101,8 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, "broker: PUBLISH fwd sock=%d -> sock=%d " "topic=%s qos=%d len=%u", (int)bc->sock, (int)sub->client->sock, - topic, eff_qos, (unsigned)pub.total_len); + BrokerLog_Sanitize(topic), eff_qos, + (unsigned)pub.total_len); (void)MqttPacket_Write(&sub->client->client, sub->client->tx_buf, sub_rc); } @@ -5061,7 +5147,8 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, "broker: PUBLISH enq sock=%d -> sock=%d " "topic=%s qos=%d len=%u", (int)bc->sock, (int)sub->client->sock, - topic, eff_qos, (unsigned)pub.total_len); + BrokerLog_Sanitize(topic), eff_qos, + (unsigned)pub.total_len); BrokerClient_DrainOutQueue(sub->client); } } @@ -5279,7 +5366,8 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) char* cn = wolfSSL_X509_get_subjectCN(peer); (void)cn; /* may be unused if logging disabled */ WBLOG_INFO(broker, "broker: TLS client cert sock=%d CN=%s", - (int)bc->sock, cn ? cn : "(unknown)"); + (int)bc->sock, + BrokerLog_Sanitize(cn ? cn : "(unknown)")); wolfSSL_X509_free(peer); } } From d46da084225e06f8b619f1009e40f81596a751eb Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 14:29:46 -0700 Subject: [PATCH 06/31] F-4305 F-4652 - Validate broker CONNECT credentials before any session-state mutation --- src/mqtt_broker.c | 239 ++++++++++++++++++------------------ tests/test_broker_connect.c | 59 +++++++++ 2 files changed, 181 insertions(+), 117 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 412f464d8..b608a108c 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -4299,6 +4299,122 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, /* Client ID uniqueness and clean session handling */ bc->clean_session = mc.clean_session; + /* Validate credentials BEFORE any session-state mutation. Storing the + * username/password and running the credential gate here ensures an + * unauthenticated peer that guesses a client_id cannot disconnect the + * victim, fire its LWT, or hijack/destroy its persisted subscriptions: + * the duplicate-takeover and orphan-reassociation logic below only runs + * once auth has passed. */ +#ifdef WOLFMQTT_BROKER_AUTH +#ifdef WOLFMQTT_STATIC_MEMORY + bc->username[0] = '\0'; + bc->password[0] = '\0'; +#endif + bc->password_len = 0; + if (mc.username) { + word16 ulen = 0; + if (MqttDecode_Num((byte*)mc.username - MQTT_DATA_LEN_SIZE, + &ulen, MQTT_DATA_LEN_SIZE) == MQTT_DATA_LEN_SIZE) { + #ifdef WOLFMQTT_STATIC_MEMORY + if (ulen >= BROKER_MAX_USERNAME_LEN) { + WBLOG_ERR(broker, + "broker: username too long (%u >= %d) sock=%d", + (unsigned)ulen, BROKER_MAX_USERNAME_LEN, + (int)bc->sock); + #ifdef WOLFMQTT_V5 + if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + ack.return_code = MQTT_REASON_BAD_USER_OR_PASS; + } + else + #endif + { + ack.return_code = + MQTT_CONNECT_ACK_CODE_REFUSED_BAD_USER_PWD; + } + goto send_connack; + } + #endif + BROKER_STORE_STR_SENSITIVE(bc->username, mc.username, ulen, + BROKER_MAX_USERNAME_LEN); + } + } + if (mc.password) { + word16 plen = 0; + if (MqttDecode_Num((byte*)mc.password - MQTT_DATA_LEN_SIZE, + &plen, MQTT_DATA_LEN_SIZE) == MQTT_DATA_LEN_SIZE) { + #ifdef WOLFMQTT_STATIC_MEMORY + if (plen >= BROKER_MAX_PASSWORD_LEN) { + WBLOG_ERR(broker, + "broker: password too long (%u >= %d) sock=%d", + (unsigned)plen, BROKER_MAX_PASSWORD_LEN, + (int)bc->sock); + #ifdef WOLFMQTT_V5 + if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + ack.return_code = MQTT_REASON_BAD_USER_OR_PASS; + } + else + #endif + { + ack.return_code = + MQTT_CONNECT_ACK_CODE_REFUSED_BAD_USER_PWD; + } + goto send_connack; + } + #endif + /* [MQTT-3.1.3.5] Password is Binary Data and may legally + * contain 0x00. The binary-sensitive store records the + * actual length in bc->password_len so wipe and compare + * paths don't fall back to XSTRLEN truncation. */ + BROKER_STORE_BIN_SENSITIVE(bc->password, bc->password_len, + mc.password, plen, BROKER_MAX_PASSWORD_LEN); + } + } + if (broker->auth_user || broker->auth_pass) { + int auth_ok = 1; + if (broker->auth_user && ( + #ifndef WOLFMQTT_STATIC_MEMORY + bc->username == NULL || + #endif + bc->username[0] == '\0' || + BrokerStrCompare(broker->auth_user, bc->username, + BROKER_MAX_USERNAME_LEN) != 0)) { + auth_ok = 0; + } + if (broker->auth_pass && ( + #ifndef WOLFMQTT_STATIC_MEMORY + bc->password == NULL || + #endif + bc->password_len == 0 || + BrokerBufCompare((const byte*)broker->auth_pass, + (int)XSTRLEN(broker->auth_pass), + (const byte*)bc->password, (int)bc->password_len, + BROKER_MAX_PASSWORD_LEN) != 0)) { + auth_ok = 0; + } + if (!auth_ok) { + WBLOG_ERR(broker, "broker: auth failed sock=%d user=%s", + (int)bc->sock, + #ifdef WOLFMQTT_STATIC_MEMORY + BrokerLog_Sanitize(bc->username[0] ? bc->username : "(null)")); + #else + BrokerLog_Sanitize((bc->username && bc->username[0]) + ? bc->username : "(null)")); + #endif + #ifdef WOLFMQTT_V5 + if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { + ack.return_code = MQTT_REASON_BAD_USER_OR_PASS; + } + else + #endif + { + ack.return_code = + MQTT_CONNECT_ACK_CODE_REFUSED_BAD_USER_PWD; + } + goto send_connack; + } + } +#endif /* WOLFMQTT_BROKER_AUTH */ + if (BROKER_STR_VALID(bc->client_id)) { BrokerClient* old; @@ -4448,129 +4564,18 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, } #endif /* WOLFMQTT_BROKER_WILL */ - /* Store credentials */ -#ifdef WOLFMQTT_BROKER_AUTH -#ifdef WOLFMQTT_STATIC_MEMORY - bc->username[0] = '\0'; - bc->password[0] = '\0'; -#endif - bc->password_len = 0; - if (mc.username) { - word16 ulen = 0; - if (MqttDecode_Num((byte*)mc.username - MQTT_DATA_LEN_SIZE, - &ulen, MQTT_DATA_LEN_SIZE) == MQTT_DATA_LEN_SIZE) { - #ifdef WOLFMQTT_STATIC_MEMORY - if (ulen >= BROKER_MAX_USERNAME_LEN) { - WBLOG_ERR(broker, - "broker: username too long (%u >= %d) sock=%d", - (unsigned)ulen, BROKER_MAX_USERNAME_LEN, - (int)bc->sock); - #ifdef WOLFMQTT_V5 - if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { - ack.return_code = MQTT_REASON_BAD_USER_OR_PASS; - } - else - #endif - { - ack.return_code = - MQTT_CONNECT_ACK_CODE_REFUSED_BAD_USER_PWD; - } - goto send_connack; - } - #endif - BROKER_STORE_STR_SENSITIVE(bc->username, mc.username, ulen, - BROKER_MAX_USERNAME_LEN); - } - } - if (mc.password) { - word16 plen = 0; - if (MqttDecode_Num((byte*)mc.password - MQTT_DATA_LEN_SIZE, - &plen, MQTT_DATA_LEN_SIZE) == MQTT_DATA_LEN_SIZE) { - #ifdef WOLFMQTT_STATIC_MEMORY - if (plen >= BROKER_MAX_PASSWORD_LEN) { - WBLOG_ERR(broker, - "broker: password too long (%u >= %d) sock=%d", - (unsigned)plen, BROKER_MAX_PASSWORD_LEN, - (int)bc->sock); - #ifdef WOLFMQTT_V5 - if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { - ack.return_code = MQTT_REASON_BAD_USER_OR_PASS; - } - else - #endif - { - ack.return_code = - MQTT_CONNECT_ACK_CODE_REFUSED_BAD_USER_PWD; - } - goto send_connack; - } - #endif - /* [MQTT-3.1.3.5] Password is Binary Data and may legally - * contain 0x00. The binary-sensitive store records the - * actual length in bc->password_len so wipe and compare - * paths don't fall back to XSTRLEN truncation. */ - BROKER_STORE_BIN_SENSITIVE(bc->password, bc->password_len, - mc.password, plen, BROKER_MAX_PASSWORD_LEN); - } - } -#endif /* WOLFMQTT_BROKER_AUTH */ - - /* Check auth before sending CONNACK. [MQTT-3.2.2-2]: when the - * accepted CleanSession=0 connection finds stored session state, - * Session Present MUST be 1; otherwise it MUST be 0. The flag is - * cleared again below for any path that overrides return_code to a - * non-zero refusal - [MQTT-3.2.2-4] requires Session Present=0 on a - * refused CONNACK. */ + /* Credentials were already validated above, before any session-state + * mutation. [MQTT-3.2.2-2]: when the accepted CleanSession=0 connection + * finds stored session state, Session Present MUST be 1; otherwise it + * MUST be 0. The flag is cleared again below for any path that overrides + * return_code to a non-zero refusal - [MQTT-3.2.2-4] requires Session + * Present=0 on a refused CONNACK. */ ack.flags = session_present ? MQTT_CONNECT_ACK_FLAG_SESSION_PRESENT : 0; ack.return_code = MQTT_CONNECT_ACK_CODE_ACCEPTED; #ifdef WOLFMQTT_V5 ack.props = NULL; #endif -#ifdef WOLFMQTT_BROKER_AUTH - if (broker->auth_user || broker->auth_pass) { - int auth_ok = 1; - if (broker->auth_user && ( - #ifndef WOLFMQTT_STATIC_MEMORY - bc->username == NULL || - #endif - bc->username[0] == '\0' || - BrokerStrCompare(broker->auth_user, bc->username, - BROKER_MAX_USERNAME_LEN) != 0)) { - auth_ok = 0; - } - if (broker->auth_pass && ( - #ifndef WOLFMQTT_STATIC_MEMORY - bc->password == NULL || - #endif - bc->password_len == 0 || - BrokerBufCompare((const byte*)broker->auth_pass, - (int)XSTRLEN(broker->auth_pass), - (const byte*)bc->password, (int)bc->password_len, - BROKER_MAX_PASSWORD_LEN) != 0)) { - auth_ok = 0; - } - if (!auth_ok) { - WBLOG_ERR(broker, "broker: auth failed sock=%d user=%s", (int)bc->sock, - #ifdef WOLFMQTT_STATIC_MEMORY - bc->username[0] ? bc->username : "(null)"); - #else - (bc->username && bc->username[0]) ? bc->username : "(null)"); - #endif - #ifdef WOLFMQTT_V5 - if (mc.protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5) { - ack.return_code = MQTT_REASON_BAD_USER_OR_PASS; - } - else - #endif - { - ack.return_code = - MQTT_CONNECT_ACK_CODE_REFUSED_BAD_USER_PWD; - } - } - } -#endif /* WOLFMQTT_BROKER_AUTH */ - #ifdef WOLFMQTT_V5 if (bc->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 && ack.return_code == MQTT_CONNECT_ACK_CODE_ACCEPTED) { diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 1e193719e..30cbabc0d 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -589,6 +589,64 @@ TEST(connect_v311_binary_password_exact_match_accepted) MqttBroker_Stop(&broker); MqttBroker_Free(&broker); } + +/* [CWE-863/CWE-639] An unauthenticated CONNECT must not mutate another + * client's session. A victim authenticates and stays connected; an attacker + * then reuses the victim's client_id with a wrong password. The broker must + * reject the attacker at the credential gate BEFORE the duplicate-takeover + * path, so the victim is never disconnected. Pre-fix, takeover ran before + * auth and closed the victim - g_clients[0].closed is the load-bearing + * assertion. */ +TEST(connect_unauth_client_id_does_not_take_over_victim) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + /* Victim: client_id "vic", user "user", pass "pass", CleanSession=1. */ + static const byte victim[] = { + 0x10, 0x1B, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0xC2, 0x00, 0x3C, + 0x00, 0x03, 'v', 'i', 'c', + 0x00, 0x04, 'u', 's', 'e', 'r', + 0x00, 0x04, 'p', 'a', 's', 's' + }; + /* Attacker: same client_id "vic", user "user", WRONG pass "bad". */ + static const byte attacker[] = { + 0x10, 0x1A, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0xC2, 0x00, 0x3C, + 0x00, 0x03, 'v', 'i', 'c', + 0x00, 0x04, 'u', 's', 'e', 'r', + 0x00, 0x03, 'b', 'a', 'd' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + broker.auth_user = "user"; + broker.auth_pass = "pass"; + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(2); + mock_client_input_append(0, victim, sizeof(victim)); + mock_client_input_append(1, attacker, sizeof(attacker)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + /* Victim authenticated and must remain connected. */ + ASSERT_TRUE(g_clients[0].out_len >= 4); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_ACCEPTED, g_clients[0].out_buf[3]); + ASSERT_FALSE(g_clients[0].closed); + /* Attacker rejected on auth, not via session takeover. */ + ASSERT_TRUE(g_clients[1].out_len >= 4); + ASSERT_EQ(MQTT_CONNECT_ACK_CODE_REFUSED_BAD_USER_PWD, + g_clients[1].out_buf[3]); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} #endif /* WOLFMQTT_BROKER_AUTH */ #ifdef WOLFMQTT_V5 @@ -2478,6 +2536,7 @@ int main(int argc, char** argv) #ifdef WOLFMQTT_BROKER_AUTH RUN_TEST(connect_v311_binary_password_with_embedded_nul_refused); RUN_TEST(connect_v311_binary_password_exact_match_accepted); + RUN_TEST(connect_unauth_client_id_does_not_take_over_victim); #endif #ifdef WOLFMQTT_V5 RUN_TEST(connect_v5_emptyid_assigned_id_emitted); From b4552407256833d2d977fe92ca1b9a54c93a1fd4 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 14:40:51 -0700 Subject: [PATCH 07/31] F-4308 F-4653 - Reject v5 zero topic alias and cap dynamic retained-message list --- src/mqtt_broker.c | 22 +++++++++++++--- src/mqtt_packet.c | 5 ++++ tests/test_broker_connect.c | 51 +++++++++++++++++++++++++++++++++++++ tests/test_mqtt_packet.c | 16 ++++++++++++ wolfmqtt/mqtt_broker.h | 1 + 5 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index b608a108c..de00c2129 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -3167,11 +3167,19 @@ static int BrokerRetained_Store(MqttBroker* broker, const char* topic, if (msg == NULL) { /* Allocate new node + topic */ int tlen = (int)XSTRLEN(topic); - msg = (BrokerRetainedMsg*)WOLFMQTT_MALLOC( - sizeof(BrokerRetainedMsg)); - if (msg == NULL) { + /* Cap the dynamic retained list so a client publishing RETAIN=1 to + * many distinct topics cannot grow it without bound and exhaust the + * heap; the static path is already bounded by BROKER_MAX_RETAINED. */ + if (broker->retained_count >= BROKER_MAX_RETAINED) { rc = MQTT_CODE_ERROR_MEMORY; } + if (rc == MQTT_CODE_SUCCESS) { + msg = (BrokerRetainedMsg*)WOLFMQTT_MALLOC( + sizeof(BrokerRetainedMsg)); + if (msg == NULL) { + rc = MQTT_CODE_ERROR_MEMORY; + } + } if (rc == MQTT_CODE_SUCCESS) { XMEMSET(msg, 0, sizeof(*msg)); msg->topic = (char*)WOLFMQTT_MALLOC((size_t)tlen + 1); @@ -3207,6 +3215,7 @@ static int BrokerRetained_Store(MqttBroker* broker, const char* topic, if (is_new) { msg->next = broker->retained; broker->retained = msg; + broker->retained_count++; } } else if (is_new && msg != NULL) { @@ -3274,6 +3283,9 @@ static void BrokerRetained_Delete(MqttBroker* broker, const char* topic) WOLFMQTT_FREE(cur->payload); } WOLFMQTT_FREE(cur); + if (broker->retained_count > 0) { + broker->retained_count--; + } found = 1; break; } @@ -3316,6 +3328,7 @@ static void BrokerRetained_FreeAll(MqttBroker* broker) cur = next; } broker->retained = NULL; + broker->retained_count = 0; #endif } #endif /* WOLFMQTT_BROKER_RETAINED */ @@ -3714,6 +3727,9 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, if (rm->topic) WOLFMQTT_FREE(rm->topic); if (rm->payload) WOLFMQTT_FREE(rm->payload); WOLFMQTT_FREE(rm); + if (broker->retained_count > 0) { + broker->retained_count--; + } rm = rm_next; continue; } diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 40c155061..580999c81 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -902,6 +902,11 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, buf += tmp; total += tmp; prop_len -= (word32)tmp; + /* [MQTT-3.3.2-7] A Topic Alias of 0 is a Protocol Error. */ + if (cur_prop->type == MQTT_PROP_TOPIC_ALIAS && + cur_prop->data_short == 0) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } break; } case MQTT_DATA_TYPE_INT: diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 30cbabc0d..ab5f0218b 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -1675,6 +1675,54 @@ TEST(broker_publish_before_connect_closes) MqttBroker_Free(&broker); } +#if defined(WOLFMQTT_BROKER_RETAINED) && !defined(WOLFMQTT_STATIC_MEMORY) +/* [CWE-400] The dynamic retained-message list must be bounded. A client that + * publishes RETAIN=1 to more than BROKER_MAX_RETAINED distinct topics must not + * grow the list past the cap - pre-fix it grew without bound, enabling + * heap-exhaustion DoS. */ +TEST(broker_retained_list_capped) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + byte pub[8]; + static const byte connect[] = { + 0x10, 0x0F, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x03, 'p', 'u', 'b' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(1); + mock_client_input_append(0, connect, sizeof(connect)); + /* Publish RETAIN=1 (QoS 0) to BROKER_MAX_RETAINED + 5 distinct topics. */ + for (i = 0; i < BROKER_MAX_RETAINED + 5; i++) { + pub[0] = 0x31; /* PUBLISH, retain=1 */ + pub[1] = 0x06; /* remain = 6 */ + pub[2] = 0x00; pub[3] = 0x03; /* topic len 3 */ + pub[4] = 'r'; + pub[5] = (byte)('0' + (i / 10)); + pub[6] = (byte)('0' + (i % 10)); + pub[7] = 'x'; /* payload */ + mock_client_input_append(0, pub, sizeof(pub)); + } + for (i = 0; i < BROKER_MAX_RETAINED + 12; i++) { + MqttBroker_Step(&broker); + } + + /* The list is capped, not grown to BROKER_MAX_RETAINED + 5. */ + ASSERT_EQ(BROKER_MAX_RETAINED, broker.retained_count); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_BROKER_RETAINED && !WOLFMQTT_STATIC_MEMORY */ + /* [MQTT-2.3.1-1] / [MQTT-4.13]: a SUBSCRIBE packet with Packet * Identifier = 0 is malformed and the broker MUST close the connection. * MqttDecode_Subscribe returns MQTT_CODE_ERROR_PACKET_ID; this test @@ -2561,6 +2609,9 @@ int main(int argc, char** argv) RUN_TEST(disconnect_invalid_fixed_header_flags_fires_will); RUN_TEST(broker_unhandled_packet_type_closes); RUN_TEST(broker_publish_before_connect_closes); +#if defined(WOLFMQTT_BROKER_RETAINED) && !defined(WOLFMQTT_STATIC_MEMORY) + RUN_TEST(broker_retained_list_capped); +#endif RUN_TEST(broker_subscribe_packet_id_zero_closes); RUN_TEST(connack_session_present_set_on_resumed_session); RUN_TEST(connack_session_present_set_on_takeover); diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index f7e837e38..94ead2fff 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -1255,6 +1255,21 @@ TEST(decode_publish_v5_subscription_id_zero_rejected) ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); ASSERT_NULL(pub.props); } + +/* [MQTT-3.3.2-7] A Topic Alias of 0 is a Protocol Error. Wire: PUBLISH QoS 0, + * topic "t", props_len=3, TOPIC_ALIAS(35)=0. */ +TEST(decode_publish_v5_topic_alias_zero_rejected) +{ + byte buf[] = { 0x30, 0x08, 0x00, 0x01, 't', 0x03, 0x23, 0x00, 0x00, 'x' }; + MqttPublish pub; + int rc; + + XMEMSET(&pub, 0, sizeof(pub)); + pub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(pub.props); +} #endif /* WOLFMQTT_V5 */ /* [MQTT-2.3.1-1] PUBLISH with QoS > 0 must carry a non-zero Packet @@ -4714,6 +4729,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_publish_v5_empty_topic_with_alias_accepted); RUN_TEST(decode_publish_v5_empty_topic_no_alias_rejected); RUN_TEST(decode_publish_v5_subscription_id_zero_rejected); + RUN_TEST(decode_publish_v5_topic_alias_zero_rejected); #endif RUN_TEST(decode_publish_qos1_packet_id_zero_rejected); RUN_TEST(decode_publish_qos2_packet_id_zero_rejected); diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index 63d3b233f..e7c9cfc64 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -637,6 +637,7 @@ typedef struct MqttBroker { BrokerSub* subs; #ifdef WOLFMQTT_BROKER_RETAINED BrokerRetainedMsg* retained; + int retained_count; #endif #ifdef WOLFMQTT_BROKER_WILL BrokerPendingWill* pending_wills; From 5b97a8dc36014c187cfb1ef53954e3892224ca9a Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 14:44:56 -0700 Subject: [PATCH 08/31] F-4529 - Add MqttClient_Subscribe broker-rejection regression test --- tests/test_mqtt_client.c | 67 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index 5f395c36a..cff02a95a 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -384,6 +384,72 @@ TEST(connect_clears_tx_buf_credentials) } } +/* Serves a pre-staged response packet (e.g. a SUBACK) one chunk per read so a + * full client request/response round-trip can run against the mock net. */ +static byte g_canned_buf[64]; +static int g_canned_len; +static int g_canned_pos; + +static int mock_net_read_canned(void *context, byte* buf, int buf_len, + int timeout_ms) +{ + int n; + (void)context; (void)timeout_ms; + n = g_canned_len - g_canned_pos; + if (n <= 0) { + return 0; /* exhausted -> MQTT_CODE_CONTINUE under nonblock */ + } + if (n > buf_len) { + n = buf_len; + } + XMEMCPY(buf, g_canned_buf + g_canned_pos, n); + g_canned_pos += n; + return n; +} + +/* [issue 3129] A broker that rejects a subscription returns a SUBACK whose + * per-topic return code has the high bit set (0x80 in v3.1.1, any reason + * code >= 0x80 in v5). MqttClient_Subscribe must surface this as + * MQTT_CODE_ERROR_SUBSCRIBE_REJECTED rather than MQTT_CODE_SUCCESS, else the + * application waits forever for messages on a filter the broker never + * installed. This pins the detection that previously had no test. */ +TEST(subscribe_broker_rejection_returns_subscribe_rejected) +{ + int rc; + int i; + MqttSubscribe subscribe; + MqttTopic topics[1]; + /* SUBACK v3.1.1: type=0x90, remain=3, packet_id=42, return_code=0x80. */ + static const byte suback[] = { 0x90, 0x03, 0x00, 0x2A, 0x80 }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; + + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, suback, sizeof(suback)); + g_canned_len = (int)sizeof(suback); + g_canned_pos = 0; + + XMEMSET(&subscribe, 0, sizeof(subscribe)); + XMEMSET(topics, 0, sizeof(topics)); + topics[0].topic_filter = "test/topic"; + topics[0].qos = MQTT_QOS_0; + subscribe.packet_id = 42; + subscribe.topic_count = 1; + subscribe.topics = topics; + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Subscribe(&test_client, &subscribe); + } + + ASSERT_EQ(MQTT_CODE_ERROR_SUBSCRIBE_REJECTED, rc); + ASSERT_EQ(MQTT_SUBSCRIBE_ACK_CODE_FAILURE, + subscribe.topics[0].return_code); +} + /* ============================================================================ * MqttClient_Disconnect Tests * ============================================================================ */ @@ -786,6 +852,7 @@ void run_mqtt_client_tests(void) /* MqttClient_Subscribe tests */ RUN_TEST(subscribe_null_client); RUN_TEST(subscribe_null_subscribe); + RUN_TEST(subscribe_broker_rejection_returns_subscribe_rejected); /* MqttClient_Unsubscribe tests */ RUN_TEST(unsubscribe_null_client); From 337f88df616c89cbc5dd1c1fb6f2667da9ecfd17 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 14:49:37 -0700 Subject: [PATCH 09/31] F-4657 F-4658 - Validate v5 response topic and cap per-message property count --- src/mqtt_packet.c | 15 ++++++++++++ tests/test_mqtt_packet.c | 53 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 580999c81..27e635c9a 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -832,6 +832,7 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, { int rc = 0; int total, tmp; + int prop_count = 0; MqttProp* cur_prop; byte* buf = pbuf; @@ -839,6 +840,12 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, while (((int)prop_len > 0) && (rc >= 0)) { + /* Bound the number of properties a single message may carry so a peer + * cannot saturate the shared property pool (CWE-770). */ + if (++prop_count > MQTT_MAX_PROPS) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); + break; + } /* Allocate a structure and add to head. */ cur_prop = MqttProps_Add(props); if (cur_prop == NULL) { @@ -947,6 +954,14 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, buf += tmp; total += tmp; prop_len -= (word32)tmp; + /* [MQTT-3.3.2-14] A Response Topic is a Topic Name and + * MUST NOT contain wildcard characters. */ + if (cur_prop->type == MQTT_PROP_RESP_TOPIC && + !MqttPacket_TopicNameValid(cur_prop->data_str.str, + cur_prop->data_str.len, + MQTT_CONNECT_PROTOCOL_LEVEL_5)) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); + } } else { /* Invalid length */ diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index 94ead2fff..2b234a2ea 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -1270,6 +1270,57 @@ TEST(decode_publish_v5_topic_alias_zero_rejected) ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); ASSERT_NULL(pub.props); } + +/* [MQTT-3.3.2-14] A Response Topic is a Topic Name and MUST NOT contain + * wildcards. Wire: PUBLISH QoS 0, topic "t", props_len=6, RESP_TOPIC(8)="a/#". */ +TEST(decode_publish_v5_response_topic_wildcard_rejected) +{ + byte buf[] = { 0x30, 0x0B, 0x00, 0x01, 't', 0x06, + 0x08, 0x00, 0x03, 'a', '/', '#', 'x' }; + MqttPublish pub; + int rc; + + XMEMSET(&pub, 0, sizeof(pub)); + pub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub); + ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); + ASSERT_NULL(pub.props); +} + +/* [CWE-770] A single message may not carry more than the internal + * MQTT_MAX_PROPS (default 30) properties; otherwise a peer can saturate the + * shared property pool. 40 User Property entries exceeds that cap. The cap + * returns MQTT_CODE_ERROR_PROPERTY, distinct from the MQTT_CODE_ERROR_MEMORY a + * pool-exhaustion would have produced before the fix. */ +TEST(decode_publish_v5_property_count_capped) +{ + byte buf[300]; + MqttPublish pub; + int rc, i, pos; + /* 40 User Property entries, 7 bytes each (k=v). */ + word32 nprops = 40; + word32 props_len = nprops * 7; + word32 remain = 3 + 2 + props_len; /* topic(3) + props_vbi(2) + props */ + + pos = 0; + buf[pos++] = 0x30; /* PUBLISH QoS 0 */ + buf[pos++] = (byte)((remain & 0x7F) | 0x80); /* remain VBI low */ + buf[pos++] = (byte)(remain >> 7); /* remain VBI high */ + buf[pos++] = 0x00; buf[pos++] = 0x01; buf[pos++] = 't'; /* topic "t" */ + buf[pos++] = (byte)((props_len & 0x7F) | 0x80); /* props_len VBI low */ + buf[pos++] = (byte)(props_len >> 7); /* props_len VBI high */ + for (i = 0; i < (int)nprops; i++) { + buf[pos++] = 0x26; /* User Property */ + buf[pos++] = 0x00; buf[pos++] = 0x01; buf[pos++] = 'k'; + buf[pos++] = 0x00; buf[pos++] = 0x01; buf[pos++] = 'v'; + } + + XMEMSET(&pub, 0, sizeof(pub)); + pub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Publish(buf, pos, &pub); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); + ASSERT_NULL(pub.props); +} #endif /* WOLFMQTT_V5 */ /* [MQTT-2.3.1-1] PUBLISH with QoS > 0 must carry a non-zero Packet @@ -4730,6 +4781,8 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_publish_v5_empty_topic_no_alias_rejected); RUN_TEST(decode_publish_v5_subscription_id_zero_rejected); RUN_TEST(decode_publish_v5_topic_alias_zero_rejected); + RUN_TEST(decode_publish_v5_response_topic_wildcard_rejected); + RUN_TEST(decode_publish_v5_property_count_capped); #endif RUN_TEST(decode_publish_qos1_packet_id_zero_rejected); RUN_TEST(decode_publish_qos2_packet_id_zero_rejected); From 2c5ce3239aaa0b8a28fcb7dedf16af7dd68cd610 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 14:55:03 -0700 Subject: [PATCH 10/31] F-4656 - Cap per-client broker subscriptions to prevent table exhaustion --- src/mqtt_broker.c | 19 +++++++++++++ tests/test_broker_connect.c | 53 +++++++++++++++++++++++++++++++++++++ wolfmqtt/mqtt_broker.h | 6 +++++ 3 files changed, 78 insertions(+) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index de00c2129..5f95afdb1 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -2732,6 +2732,7 @@ static void BrokerSubs_RemoveClient(MqttBroker* broker, BrokerClient* bc) cur = next; } #endif + bc->sub_count = 0; #ifdef WOLFMQTT_BROKER_PERSIST /* Clean-session disconnect drops the persistent record. For @@ -2786,6 +2787,14 @@ static int BrokerSubs_Add(MqttBroker* broker, BrokerClient* bc, } #endif + /* Per-client cap: prevent a single client from occupying the whole shared + * subscription table and denying SUBSCRIBE to other clients. */ + if (bc->sub_count >= BROKER_MAX_SUBS_PER_CLIENT) { + WBLOG_ERR(broker, "broker: sub cap reached sock=%d (max %d)", + (int)bc->sock, BROKER_MAX_SUBS_PER_CLIENT); + return MQTT_CODE_ERROR_MEMORY; + } + #ifdef WOLFMQTT_STATIC_MEMORY for (i = 0; i < BROKER_MAX_SUBS; i++) { if (!broker->subs[i].in_use) { @@ -2852,6 +2861,7 @@ static int BrokerSubs_Add(MqttBroker* broker, BrokerClient* bc, } } #endif + bc->sub_count++; WBLOG_INFO(broker, "broker: sub add sock=%d filter=%s qos=%d", (int)bc->sock, BrokerLog_Sanitize(sub->filter), qos); } @@ -2878,6 +2888,9 @@ static void BrokerSubs_Remove(MqttBroker* broker, BrokerClient* bc, WBLOG_INFO(broker, "broker: sub remove sock=%d filter=%s", (int)bc->sock, BrokerLog_Sanitize(s->filter)); XMEMSET(s, 0, sizeof(BrokerSub)); + if (bc->sub_count > 0) { + bc->sub_count--; + } return; } } @@ -2902,6 +2915,9 @@ static void BrokerSubs_Remove(MqttBroker* broker, BrokerClient* bc, WOLFMQTT_FREE(cur->client_id); } WOLFMQTT_FREE(cur); + if (bc->sub_count > 0) { + bc->sub_count--; + } return; } prev = cur; @@ -3086,6 +3102,9 @@ static int BrokerSubs_ReassociateClient(MqttBroker* broker, } #endif if (count > 0) { + /* The new client now owns these subs; reflect them in its cap count + * so a reconnect cannot be used to exceed BROKER_MAX_SUBS_PER_CLIENT. */ + new_bc->sub_count += count; WBLOG_INFO(broker, "broker: reassociated %d subs for client_id=%s", count, BrokerLog_Sanitize(client_id)); } diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index ab5f0218b..98670a3f6 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -1723,6 +1723,56 @@ TEST(broker_retained_list_capped) } #endif /* WOLFMQTT_BROKER_RETAINED && !WOLFMQTT_STATIC_MEMORY */ +#ifndef WOLFMQTT_STATIC_MEMORY +/* [CWE-770] A single client cannot occupy more than BROKER_MAX_SUBS_PER_CLIENT + * slots in the shared subscription table; excess SUBSCRIBEs are refused so + * other clients are not denied service. */ +TEST(broker_per_client_subscription_cap) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + byte sub[10]; + static const byte connect[] = { + 0x10, 0x0F, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x04, 0x02, 0x00, 0x3C, + 0x00, 0x03, 's', 'u', 'b' + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(1); + mock_client_input_append(0, connect, sizeof(connect)); + /* Subscribe to BROKER_MAX_SUBS_PER_CLIENT + 3 distinct filters. */ + for (i = 0; i < BROKER_MAX_SUBS_PER_CLIENT + 3; i++) { + sub[0] = 0x82; /* SUBSCRIBE */ + sub[1] = 0x08; /* remain = 8 */ + sub[2] = (byte)((i + 1) >> 8); /* packet_id hi */ + sub[3] = (byte)((i + 1) & 0xFF); /* packet_id lo */ + sub[4] = 0x00; sub[5] = 0x03; /* filter len 3 */ + sub[6] = 'f'; + sub[7] = (byte)('0' + (i / 10)); + sub[8] = (byte)('0' + (i % 10)); + sub[9] = 0x00; /* options: QoS 0 */ + mock_client_input_append(0, sub, sizeof(sub)); + } + for (i = 0; i < BROKER_MAX_SUBS_PER_CLIENT + 12; i++) { + MqttBroker_Step(&broker); + } + + /* Capped, not grown to BROKER_MAX_SUBS_PER_CLIENT + 3. */ + ASSERT_TRUE(broker.clients != NULL); + ASSERT_EQ(BROKER_MAX_SUBS_PER_CLIENT, broker.clients->sub_count); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* !WOLFMQTT_STATIC_MEMORY */ + /* [MQTT-2.3.1-1] / [MQTT-4.13]: a SUBSCRIBE packet with Packet * Identifier = 0 is malformed and the broker MUST close the connection. * MqttDecode_Subscribe returns MQTT_CODE_ERROR_PACKET_ID; this test @@ -2611,6 +2661,9 @@ int main(int argc, char** argv) RUN_TEST(broker_publish_before_connect_closes); #if defined(WOLFMQTT_BROKER_RETAINED) && !defined(WOLFMQTT_STATIC_MEMORY) RUN_TEST(broker_retained_list_capped); +#endif +#ifndef WOLFMQTT_STATIC_MEMORY + RUN_TEST(broker_per_client_subscription_cap); #endif RUN_TEST(broker_subscribe_packet_id_zero_closes); RUN_TEST(connack_session_present_set_on_resumed_session); diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index e7c9cfc64..b2c1a3238 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -93,6 +93,11 @@ #ifndef BROKER_MAX_SUBS #define BROKER_MAX_SUBS 32 #endif +/* Per-client subscription cap so one client cannot occupy the whole shared + * subscription table and deny other clients (CWE-770). */ +#ifndef BROKER_MAX_SUBS_PER_CLIENT + #define BROKER_MAX_SUBS_PER_CLIENT (BROKER_MAX_SUBS / BROKER_MAX_CLIENTS) +#endif #ifndef BROKER_MAX_CLIENT_ID_LEN #define BROKER_MAX_CLIENT_ID_LEN 64 #endif @@ -474,6 +479,7 @@ typedef struct BrokerClient { WOLFMQTT_BROKER_TIME_T last_rx; byte clean_session; byte connected; /* set after successful CONNECT handshake */ + int sub_count; /* active subscriptions owned by this client */ #ifdef WOLFMQTT_BROKER_WILL byte has_will; word16 will_payload_len; From d9e79ce83785f876f148556bd6337e58d1860ded Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 16:30:15 -0700 Subject: [PATCH 11/31] F-4928 F-4929 F-4991 - Enforce TLS peer verification in example verify callbacks --- examples/aws/awsiot.c | 15 +++++++++++---- examples/mqttexample.c | 13 +++++++++---- examples/mqttsimple/mqttsimple.c | 13 +++++++++---- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/examples/aws/awsiot.c b/examples/aws/awsiot.c index bf4c57d9c..971b8162b 100644 --- a/examples/aws/awsiot.c +++ b/examples/aws/awsiot.c @@ -291,11 +291,18 @@ static int mqtt_aws_tls_verify_cb(int preverify, WOLFSSL_X509_STORE_CTX* store) PRINTF(" Rejecting cert: verification must succeed under" " WOLFSSL_NO_ASN_STRICT"); return 0; +#elif defined(WOLFMQTT_ALLOW_INSECURE_TLS) + /* Development/testing override only: strict ASN parsing drops + * Starfield Services Root CA G2 (serialNumber=0), so the chain can + * legitimately fail here. Accept anyway to keep the demo running. + * MUST NOT be defined in production - it disables authentication. */ + PRINTF(" Allowing cert anyways (WOLFMQTT_ALLOW_INSECURE_TLS)"); #else - /* Strict ASN parsing drops Starfield Services Root CA G2 - * (serialNumber=0), so chain verification can legitimately - * fail here. Keep the demo running. */ - PRINTF(" Allowing cert anyways"); + /* Reject on any verification error by default. To run the AWS IoT + * demo against the live endpoint, build with WOLFSSL_NO_ASN_STRICT + * (loads the full trust bundle) or supply a trusted chain. */ + PRINTF(" Rejecting cert: verification failed"); + return 0; #endif } diff --git a/examples/mqttexample.c b/examples/mqttexample.c index cd667d52e..b5c7c0af5 100644 --- a/examples/mqttexample.c +++ b/examples/mqttexample.c @@ -632,13 +632,18 @@ static int mqtt_tls_verify_cb(int preverify, WOLFSSL_X509_STORE_CTX* store) wolfSSL_ERR_error_string(store->error, buffer) : "none"); PRINTF(" Subject's domain name is %s", store->domain); +#ifdef WOLFMQTT_ALLOW_INSECURE_TLS + /* Development/testing override only: accept any certificate. MUST NOT be + * defined in production builds - it disables server authentication. */ if (store->error != 0) { - /* Allowing to continue */ - /* Should check certificate and return 0 if not okay */ - PRINTF(" Allowing cert anyways"); + PRINTF(" Allowing cert anyways (WOLFMQTT_ALLOW_INSECURE_TLS)"); } - return 1; +#else + /* Propagate wolfSSL's chain-validation result so a bad certificate + * (self-signed, expired, wrong host, untrusted CA) fails the handshake. */ + return preverify; +#endif } /* Use this callback to setup TLS certificates and verify callbacks */ diff --git a/examples/mqttsimple/mqttsimple.c b/examples/mqttsimple/mqttsimple.c index 9b9769edc..ebaf00912 100644 --- a/examples/mqttsimple/mqttsimple.c +++ b/examples/mqttsimple/mqttsimple.c @@ -295,13 +295,18 @@ static int mqtt_tls_verify_cb(int preverify, WOLFSSL_X509_STORE_CTX* store) wolfSSL_ERR_error_string(store->error, buffer) : "none"); PRINTF(" Subject's domain name is %s", store->domain); +#ifdef WOLFMQTT_ALLOW_INSECURE_TLS + /* Development/testing override only: accept any certificate. MUST NOT be + * defined in production builds - it disables server authentication. */ if (store->error != 0) { - /* Allowing to continue */ - /* Should check certificate and return 0 if not okay */ - PRINTF(" Allowing cert anyways"); + PRINTF(" Allowing cert anyways (WOLFMQTT_ALLOW_INSECURE_TLS)"); } - return 1; +#else + /* Propagate wolfSSL's chain-validation result so a bad certificate + * (self-signed, expired, wrong host, untrusted CA) fails the handshake. */ + return preverify; +#endif } /* Use this callback to setup TLS certificates and verify callbacks */ From 087d9f0130d7e6b5e86ab1778f83e1dd8eacc077 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 16:34:56 -0700 Subject: [PATCH 12/31] F-5766 F-5767 F-5768 F-5769 F-5861 - Sanitize broker-controlled strings in example log output --- examples/aws/awsiot.c | 38 ++++++++++++++++++++++++------ examples/azure/azureiothub.c | 6 +++-- examples/mqttsimple/mqttsimple.c | 6 +++-- examples/multithread/multithread.c | 6 +++-- examples/wiot/wiot.c | 6 +++-- 5 files changed, 47 insertions(+), 15 deletions(-) diff --git a/examples/aws/awsiot.c b/examples/aws/awsiot.c index 971b8162b..5d1c4b547 100644 --- a/examples/aws/awsiot.c +++ b/examples/aws/awsiot.c @@ -51,6 +51,7 @@ #include "awsiot.h" #include "examples/mqttexample.h" #include "examples/mqttnet.h" +#include "examples/mqtt_log.h" #include /* Locals */ @@ -359,6 +360,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, { MQTTCtx* mqttCtx = (MQTTCtx*)client->ctx; byte buf[PRINT_BUFFER_SIZE+1]; + char safebuf[PRINT_BUFFER_SIZE+1]; word32 len; (void)mqttCtx; @@ -374,7 +376,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, /* Print incoming message */ PRINTF("MQTT Message: Topic %s, Qos %d, Len %u", - buf, msg->qos, msg->total_len); + mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf), msg->qos, msg->total_len); } /* Print message payload */ @@ -385,7 +387,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, XMEMCPY(buf, msg->buffer, len); buf[len] = '\0'; /* Make sure its null terminated */ PRINTF("Payload (%d - %d) printing %d bytes:" LINE_END "%s", - msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, buf); + msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf)); if (msg_done) { PRINTF("MQTT Message: Done"); @@ -402,11 +404,30 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, /* The property callback is called after decoding a packet that contains at least one property. The property list is deallocated after returning from the callback. */ +/* Copy a length-delimited, broker-controlled property string into dst and + * sanitize it for safe logging (CWE-117). */ +static const char* awsiot_log_prop(char* dst, word32 dstLen, + const char* src, word32 srcLen) +{ + char tmp[PRINT_BUFFER_SIZE + 1]; + word32 n = srcLen; + if (n > PRINT_BUFFER_SIZE) { + n = PRINT_BUFFER_SIZE; + } + if (src != NULL && n > 0) { + XMEMCPY(tmp, src, n); + } + tmp[n] = '\0'; + return mqtt_log_sanitize(dst, dstLen, tmp); +} + static int mqtt_property_cb(MqttClient *client, MqttProp *head, void *ctx) { MqttProp *prop = head; int rc = 0; MQTTCtx* mqttCtx; + char safebuf[PRINT_BUFFER_SIZE + 1]; + char safebuf2[PRINT_BUFFER_SIZE + 1]; if ((client == NULL) || (client->ctx == NULL)) { return MQTT_CODE_ERROR_BAD_ARG; @@ -454,14 +475,17 @@ static int mqtt_property_cb(MqttClient *client, MqttProp *head, void *ctx) break; case MQTT_PROP_REASON_STR: - PRINTF("Reason String: %.*s", - prop->data_str.len, prop->data_str.str); + PRINTF("Reason String: %s", + awsiot_log_prop(safebuf, (word32)sizeof(safebuf), + prop->data_str.str, prop->data_str.len)); break; case MQTT_PROP_USER_PROP: - PRINTF("User property: key=\"%.*s\", value=\"%.*s\"", - prop->data_str.len, prop->data_str.str, - prop->data_str2.len, prop->data_str2.str); + PRINTF("User property: key=\"%s\", value=\"%s\"", + awsiot_log_prop(safebuf, (word32)sizeof(safebuf), + prop->data_str.str, prop->data_str.len), + awsiot_log_prop(safebuf2, (word32)sizeof(safebuf2), + prop->data_str2.str, prop->data_str2.len)); break; case MQTT_PROP_ASSIGNED_CLIENT_ID: diff --git a/examples/azure/azureiothub.c b/examples/azure/azureiothub.c index b80bf97e1..23491cc38 100644 --- a/examples/azure/azureiothub.c +++ b/examples/azure/azureiothub.c @@ -65,6 +65,7 @@ #include "azureiothub.h" #include "examples/mqttexample.h" #include "examples/mqttnet.h" +#include "examples/mqtt_log.h" /* Locals */ static int mStopRead = 0; @@ -134,6 +135,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, { MQTTCtx* mqttCtx = (MQTTCtx*)client->ctx; byte buf[PRINT_BUFFER_SIZE+1]; + char safebuf[PRINT_BUFFER_SIZE+1]; word32 len; (void)mqttCtx; @@ -149,7 +151,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, /* Print incoming message */ PRINTF("MQTT Message: Topic %s, Qos %d, Len %u", - buf, msg->qos, msg->total_len); + mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf), msg->qos, msg->total_len); } /* Print message payload */ @@ -160,7 +162,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, XMEMCPY(buf, msg->buffer, len); buf[len] = '\0'; /* Make sure its null terminated */ PRINTF("Payload (%d - %d) printing %d bytes:" LINE_END "%s", - msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, buf); + msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf)); if (msg_done) { PRINTF("MQTT Message: Done"); diff --git a/examples/mqttsimple/mqttsimple.c b/examples/mqttsimple/mqttsimple.c index ebaf00912..ef23c0852 100644 --- a/examples/mqttsimple/mqttsimple.c +++ b/examples/mqttsimple/mqttsimple.c @@ -28,6 +28,7 @@ #include "wolfmqtt/mqtt_client.h" #include "examples/mqttport.h" +#include "examples/mqtt_log.h" #include "mqttsimple.h" /* Requires BSD Style Socket */ @@ -84,6 +85,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, byte msg_new, byte msg_done) { byte buf[PRINT_BUFFER_SIZE+1]; + char safebuf[PRINT_BUFFER_SIZE+1]; word32 len; (void)client; @@ -99,7 +101,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, /* Print incoming message */ PRINTF("MQTT Message: Topic %s, Qos %d, Len %u", - buf, msg->qos, msg->total_len); + mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf), msg->qos, msg->total_len); } /* Print message payload */ @@ -110,7 +112,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, XMEMCPY(buf, msg->buffer, len); buf[len] = '\0'; /* Make sure its null terminated */ PRINTF("Payload (%d - %d) printing %d bytes:" LINE_END "%s", - msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, buf); + msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf)); if (msg_done) { PRINTF("MQTT Message: Done"); diff --git a/examples/multithread/multithread.c b/examples/multithread/multithread.c index 9901a21e5..83df6bdcc 100644 --- a/examples/multithread/multithread.c +++ b/examples/multithread/multithread.c @@ -28,6 +28,7 @@ #include "multithread.h" #include "examples/mqttnet.h" +#include "examples/mqtt_log.h" #include "examples/mqttexample.h" #include @@ -165,6 +166,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, byte msg_new, byte msg_done) { byte buf[PRINT_BUFFER_SIZE+1]; + char safebuf[PRINT_BUFFER_SIZE+1]; word32 len; MQTTCtx* mqttCtx = (MQTTCtx*)client->ctx; (void)mqttCtx; @@ -181,7 +183,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, /* Print incoming message */ PRINTF("MQTT Message: Topic %s, Qos %d, Id %d, Len %u, %u, %u", - buf, msg->qos, msg->packet_id, msg->total_len, msg->buffer_len, + mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf), msg->qos, msg->packet_id, msg->total_len, msg->buffer_len, msg->buffer_pos); } @@ -193,7 +195,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, XMEMCPY(buf, msg->buffer, len); buf[len] = '\0'; /* Make sure its null terminated */ PRINTF("Payload (%d - %d) printing %d bytes:" LINE_END "%s", - msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, buf); + msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf)); if (msg_done) { /* for test mode: count the number of messages received */ diff --git a/examples/wiot/wiot.c b/examples/wiot/wiot.c index f426705a3..e23272aa9 100644 --- a/examples/wiot/wiot.c +++ b/examples/wiot/wiot.c @@ -35,6 +35,7 @@ #include "wiot.h" #include "examples/mqttexample.h" #include "examples/mqttnet.h" +#include "examples/mqtt_log.h" /* Locals */ static int mStopRead = 0; @@ -82,6 +83,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, byte msg_new, byte msg_done) { byte buf[PRINT_BUFFER_SIZE+1]; + char safebuf[PRINT_BUFFER_SIZE+1]; word32 len; MQTTCtx* mqttCtx = (MQTTCtx*)client->ctx; @@ -98,7 +100,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, /* Print incoming message */ PRINTF("MQTT Message: Topic %s, Qos %d, Len %u", - buf, msg->qos, msg->total_len); + mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf), msg->qos, msg->total_len); /* for test mode: check if TEST_MESSAGE was received */ if (mqttCtx->test_mode) { @@ -117,7 +119,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, XMEMCPY(buf, msg->buffer, len); buf[len] = '\0'; /* Make sure its null terminated */ PRINTF("Payload (%d - %d) printing %d bytes:" LINE_END "%s", - msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, buf); + msg->buffer_pos, msg->buffer_pos + msg->buffer_len, len, mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf)); if (msg_done) { PRINTF("MQTT Message: Done"); From c829dbcaa648ccb0b98661b51f9bdb6ec0cbdd35 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 16:40:23 -0700 Subject: [PATCH 13/31] F-4724 F-4772 - Fix firmware client length-overflow and topic-gate bypass --- examples/firmware/fwclient.c | 37 ++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/examples/firmware/fwclient.c b/examples/firmware/fwclient.c index d90fcb509..3c70c75c0 100644 --- a/examples/firmware/fwclient.c +++ b/examples/firmware/fwclient.c @@ -118,13 +118,31 @@ static int fw_message_process(MQTTCtx *mqttCtx, byte* buffer, word32 len) #ifdef ENABLE_FIRMWARE_SIG ecc_key eccKey; #endif - word32 check_len = sizeof(FirmwareHeader) + header->sigLen + - header->pubKeyLen + header->fwLen; - - /* Verify entire message was received */ - if (len != check_len) { + word32 remaining; + + /* Validate the field sizes sequentially against the received length. + * A summed length check (sizeof(header) + sigLen + pubKeyLen + fwLen) + * overflows word32 for attacker-chosen fwLen, letting a too-short buffer + * pass and leaving pubKeyBuf/fwBuf pointing past the allocation + * (CWE-190 -> heap OOB read/write). */ + if (len < sizeof(FirmwareHeader)) { + PRINTF("Message smaller than firmware header! %d", len); + return EXIT_FAILURE; + } + remaining = len - sizeof(FirmwareHeader); + if (header->sigLen > remaining) { + PRINTF("Firmware sigLen exceeds message! %d", header->sigLen); + return EXIT_FAILURE; + } + remaining -= header->sigLen; + if (header->pubKeyLen > remaining) { + PRINTF("Firmware pubKeyLen exceeds message! %d", header->pubKeyLen); + return EXIT_FAILURE; + } + remaining -= header->pubKeyLen; + if (header->fwLen != remaining) { PRINTF("Message header vs. actual size mismatch! %d != %d", - len, check_len); + header->fwLen, remaining); return EXIT_FAILURE; } @@ -172,10 +190,13 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, { MQTTCtx* mqttCtx = (MQTTCtx*)client->ctx; - /* Verify this message is for the firmware topic */ + /* Verify this message is for the firmware topic. Compare against the full + * expected length (not the wire-supplied topic_name_len) so a zero-length + * (v5 Topic Alias) or byte-prefix topic cannot pass the gate. */ if (msg_new && + msg->topic_name_len == (word16)XSTRLEN(mqttCtx->topic_name) && XSTRNCMP(msg->topic_name, mqttCtx->topic_name, - msg->topic_name_len) == 0 && + XSTRLEN(mqttCtx->topic_name)) == 0 && !mFwBuf) { /* Allocate buffer for entire message */ From 154bfaed936585700466c83803913816e19cebbf Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 16:43:08 -0700 Subject: [PATCH 14/31] F-5148 - Prevent broker client double-free on re-entrant takeover close --- src/mqtt_broker.c | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 5f95afdb1..9a1cce4e5 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -2159,6 +2159,7 @@ static void BrokerClient_Remove(MqttBroker* broker, BrokerClient* bc) #ifndef WOLFMQTT_STATIC_MEMORY BrokerClient* cur; BrokerClient* prev = NULL; + int found = 0; #endif if (broker == NULL || bc == NULL) { @@ -2175,13 +2176,21 @@ static void BrokerClient_Remove(MqttBroker* broker, BrokerClient* bc) else { broker->clients = cur->next; } + found = 1; break; } prev = cur; cur = cur->next; } -#endif + /* Only free when bc was actually unlinked. A re-entrant close callback + * (e.g. WebSocket LWS_CALLBACK_CLOSED during a takeover fan-out) can have + * already removed and freed bc; freeing again here would double-free. */ + if (found) { + BrokerClient_Free(bc); + } +#else BrokerClient_Free(bc); +#endif } /* -------------------------------------------------------------------------- */ @@ -4461,6 +4470,15 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, WBLOG_INFO(broker, "broker: duplicate client_id=%s, disconnecting " "old sock=%d", BrokerLog_Sanitize(bc->client_id), (int)old->sock); +#ifdef ENABLE_MQTT_WEBSOCKET + /* Guard old across its takeover fan-out: a re-entrant + * LWS_CALLBACK_CLOSED for old's dropped socket must take the + * deferred-remove path instead of freeing old mid-takeover, which + * would UAF (BrokerSubs_RemoveClient) and double-free below. */ + if (old->ws_ctx != NULL) { + ((BrokerWsCtx*)old->ws_ctx)->processing = 1; + } +#endif /* Publish old client's will on takeover */ #ifdef WOLFMQTT_V5 if (old->protocol_level < MQTT_CONNECT_PROTOCOL_LEVEL_5) { @@ -4473,6 +4491,11 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len, } #else BrokerClient_PublishWill(broker, old); +#endif +#ifdef ENABLE_MQTT_WEBSOCKET + if (old->ws_ctx != NULL) { + ((BrokerWsCtx*)old->ws_ctx)->processing = 0; + } #endif if (!mc.clean_session) { /* Reassociate old client's subs to new client */ From be003afa6c9342f317a4e0f92126f08d343fba98 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 17:06:45 -0700 Subject: [PATCH 15/31] F-4773 F-4992 F-5116 F-5143 F-5771 - Fix WebSocket fan-out UAF, TLS verification, and log injection --- examples/websocket/net_libwebsockets.c | 16 +++++++++-- examples/websocket/websocket_client.c | 8 ++++-- src/mqtt_broker.c | 38 +++++++++++++++++++++++--- wolfmqtt/mqtt_broker.h | 2 ++ 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/examples/websocket/net_libwebsockets.c b/examples/websocket/net_libwebsockets.c index c005acd1a..35deefaa4 100644 --- a/examples/websocket/net_libwebsockets.c +++ b/examples/websocket/net_libwebsockets.c @@ -68,8 +68,13 @@ static int callback_mqtt(struct lws *wsi, enum lws_callback_reasons reason, else if (reason == LWS_CALLBACK_CLIENT_CONNECTION_ERROR) { net->status = -1; } - else if (reason == LWS_CALLBACK_CLOSED) { + else if (reason == LWS_CALLBACK_CLOSED || + reason == LWS_CALLBACK_CLIENT_CLOSED) { net->status = 0; + /* libwebsockets frees the wsi after this callback returns; clear the + * dangling pointer so NetWebsocket_Disconnect's `if (net->wsi)` guard + * skips lws_close_reason() on freed memory (CWE-416). */ + net->wsi = NULL; } else if (reason == LWS_CALLBACK_CLIENT_RECEIVE) { if (in && len > 0) { @@ -185,8 +190,13 @@ int NetWebsocket_Connect(void *ctx, const char* host, word16 port, /* Set SSL options for the connection if TLS is enabled */ if (mqttCtx && mqttCtx->use_tls) { conn_info.ssl_connection = LCCSCF_USE_SSL; - conn_info.ssl_connection |= LCCSCF_ALLOW_SELFSIGNED; - conn_info.ssl_connection |= LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK; + /* Only relax verification when no CA was supplied (dev/self-signed). + * When the operator provides a CA via -A, perform full chain and + * hostname verification rather than silently disabling it. */ + if (mqttCtx->ca_file == NULL) { + conn_info.ssl_connection |= LCCSCF_ALLOW_SELFSIGNED; + conn_info.ssl_connection |= LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK; + } } #endif /* ENABLE_MQTT_TLS */ diff --git a/examples/websocket/websocket_client.c b/examples/websocket/websocket_client.c index 46211a5dd..3db8d463b 100644 --- a/examples/websocket/websocket_client.c +++ b/examples/websocket/websocket_client.c @@ -27,6 +27,7 @@ #include "wolfmqtt/mqtt_client.h" #include "examples/mqttnet.h" #include "examples/mqttexample.h" +#include "examples/mqtt_log.h" #include "examples/websocket/net_libwebsockets.h" #include #include @@ -46,6 +47,7 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, byte msg_new, byte msg_done) { byte buf[PRINT_BUFFER_SIZE+1]; + char safebuf[PRINT_BUFFER_SIZE+1]; word32 len; (void)client; @@ -61,7 +63,8 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, /* Print topic */ printf("MQTT Message: Topic %s, Qos %d, Len %u", - buf, msg->qos, msg->total_len); + mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf), + msg->qos, msg->total_len); } /* Print message payload */ @@ -73,7 +76,8 @@ static int mqtt_message_cb(MqttClient *client, MqttMessage *msg, buf[len] = '\0'; /* Make sure it's null terminated */ printf("Payload (%d - %d): %s\n", - msg->buffer_pos, msg->buffer_pos + msg->buffer_len, buf); + msg->buffer_pos, msg->buffer_pos + msg->buffer_len, + mqtt_log_sanitize(safebuf, (word32)sizeof(safebuf), (char*)buf)); if (msg_done) { printf("MQTT Message: Done\n"); diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 9a1cce4e5..bdbd702e8 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -3300,6 +3300,15 @@ static void BrokerRetained_Delete(MqttBroker* broker, const char* topic) if (cur->topic != NULL && XSTRCMP(cur->topic, topic) == 0) { WBLOG_DBG(broker, "broker: retained delete topic=%s", BrokerLog_Sanitize(topic)); + if (broker->retained_delivering > 0) { + /* A delivery loop is iterating this list (possibly re-entered + * via a WebSocket fan-out). Freeing now would invalidate that + * loop's saved next pointer (CWE-416); flag for deferred reap + * by the delivery loop instead. */ + cur->pending_delete = 1; + found = 1; + break; + } if (prev) { prev->next = next; } @@ -3695,6 +3704,13 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, } now = WOLFMQTT_BROKER_GET_TIME_S(); +#ifndef WOLFMQTT_STATIC_MEMORY + /* Mark a delivery in progress so a re-entrant BrokerRetained_Delete (via a + * WebSocket fan-out close) defers its free instead of invalidating the + * loop's saved next pointer. */ + broker->retained_delivering++; +#endif + #ifdef WOLFMQTT_STATIC_MEMORY for (i = 0; i < BROKER_MAX_RETAINED; i++) { BrokerRetainedMsg* rm = &broker->retained[i]; @@ -3741,9 +3757,11 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, rm = broker->retained; while (rm) { BrokerRetainedMsg* rm_next = rm->next; - /* Skip and remove expired messages */ - if (rm->expiry_sec > 0 && - (now - rm->store_time) >= rm->expiry_sec) { + /* Reap expired messages and any nodes a re-entrant delete deferred + * (pending_delete). The free here is safe: rm_next is already saved. */ + if (rm->pending_delete || + (rm->expiry_sec > 0 && + (now - rm->store_time) >= rm->expiry_sec)) { WBLOG_DBG(broker, "broker: retained expired topic=%s", BrokerLog_Sanitize(rm->topic)); if (rm_prev) { @@ -3792,6 +3810,12 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, rm = rm_next; } #endif + +#ifndef WOLFMQTT_STATIC_MEMORY + if (broker->retained_delivering > 0) { + broker->retained_delivering--; + } +#endif } #endif /* WOLFMQTT_BROKER_RETAINED */ @@ -3839,6 +3863,7 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, int i; #else BrokerSub* sub; + BrokerSub* next_sub = NULL; #endif if (broker == NULL || topic == NULL) { @@ -3868,6 +3893,11 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, #else sub = broker->subs; while (sub) { + /* Snapshot the successor before any MqttPacket_Write: a WS fan-out + * write can drive an lws_service spin whose re-entrant CLOSED frees + * this client's BrokerSub nodes, so reading sub->next afterwards + * would dereference a freed node. */ + next_sub = sub->next; #endif if (sub->client != NULL && sub->client->protocol_level != 0 && BROKER_STR_VALID(sub->filter) && @@ -3897,7 +3927,7 @@ static void BrokerClient_PublishWillImmediate(MqttBroker* broker, } } #ifndef WOLFMQTT_STATIC_MEMORY - sub = sub->next; + sub = next_sub; #endif } } diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index b2c1a3238..94ec97691 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -576,6 +576,7 @@ typedef struct BrokerRetainedMsg { WOLFMQTT_BROKER_TIME_T store_time; /* when stored (seconds) */ word32 expiry_sec; /* v5 message expiry (0=none) */ MqttQoS qos; /* [MQTT-3.3.1-5] stored QoS */ + byte pending_delete; /* deferred free during delivery */ } BrokerRetainedMsg; #endif /* WOLFMQTT_BROKER_RETAINED */ @@ -644,6 +645,7 @@ typedef struct MqttBroker { #ifdef WOLFMQTT_BROKER_RETAINED BrokerRetainedMsg* retained; int retained_count; + int retained_delivering; /* re-entrancy guard for delete */ #endif #ifdef WOLFMQTT_BROKER_WILL BrokerPendingWill* pending_wills; From 61870fb87e30d332fd5a8695a5c4a87693de68eb Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 17:14:28 -0700 Subject: [PATCH 16/31] F-4726 F-5115 - Enforce WebSocket origin allowlist and TLS-only listener policy --- src/mqtt_broker.c | 29 ++++++++++++++++++++++++++++- wolfmqtt/mqtt_broker.h | 4 ++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index bdbd702e8..afc1a880c 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -1007,7 +1007,24 @@ static int callback_broker_mqtt(struct lws *wsi, (void)user; - if (reason == LWS_CALLBACK_ESTABLISHED) { + if (reason == LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION) { + /* CSWSH defense: when an Origin allowlist is configured, reject a + * browser-supplied Origin that does not match exactly. Requests with + * no Origin header (native clients) are not browser-originated and are + * allowed through. */ + if (broker != NULL && broker->ws_allowed_origin != NULL) { + char origin[256]; + int olen = lws_hdr_copy(wsi, origin, (int)sizeof(origin), + WSI_TOKEN_ORIGIN); + if (olen > 0 && + XSTRCMP(origin, broker->ws_allowed_origin) != 0) { + WBLOG_ERR(broker, "broker: ws origin rejected: %s", + BrokerLog_Sanitize(origin)); + return -1; + } + } + } + else if (reason == LWS_CALLBACK_ESTABLISHED) { bc = BrokerClient_AddWs(broker, wsi); if (bc == NULL) { WBLOG_ERR(broker, "broker: ws accept rejected (alloc)"); @@ -6059,6 +6076,16 @@ int MqttBroker_Start(MqttBroker* broker) #ifdef ENABLE_MQTT_WEBSOCKET if (broker->use_websocket) { + #ifdef WOLFMQTT_BROKER_NO_INSECURE + /* TLS-only build: a plaintext WebSocket listener would silently bypass + * the policy the plain-TCP listener enforces. Require WSS. */ + if (broker->ws_tls_cert == NULL) { + WBLOG_ERR(broker, "broker: plaintext WebSocket listener refused in " + "TLS-only build (WOLFMQTT_BROKER_NO_INSECURE); set ws_tls_cert " + "for WSS"); + return MQTT_CODE_ERROR_BAD_ARG; + } + #endif rc = BrokerWs_Init(broker); if (rc != MQTT_CODE_SUCCESS) { WBLOG_ERR(broker, "broker: WebSocket init failed rc=%d", rc); diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index 94ec97691..dfe9a88bf 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -658,6 +658,10 @@ typedef struct MqttBroker { const char *ws_tls_cert; const char *ws_tls_key; const char *ws_tls_ca; + /* Optional exact Origin allowlist for browser WebSocket connections. When + * non-NULL, a request whose HTTP Origin header is present and does not + * match is rejected (CSWSH defense). NULL = no Origin enforcement. */ + const char *ws_allowed_origin; #endif #ifdef WOLFMQTT_BROKER_PERSIST /* Pointer (not embedded struct) so the broker stays small when no From dc8ab1e09d5c9c9ce4b81f851dd5dc3a0e128e5d Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 17:20:16 -0700 Subject: [PATCH 17/31] F-4994 F-4996 F-5149 - Clamp publish payload copy, reject duplicate v5 props, fix packet-size comparison --- src/mqtt_broker.c | 1 + src/mqtt_packet.c | 40 +++++++++++++++++++++++++++++++++++++-- tests/test_mqtt_packet.c | 41 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index afc1a880c..8f00533c0 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -5197,6 +5197,7 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, out_pub.duplicate = 0; out_pub.buffer = payload; out_pub.total_len = pub.total_len; + out_pub.buffer_len = pub.buffer_len; #ifdef WOLFMQTT_V5 out_pub.protocol_level = sub->client->protocol_level; if (sub->client->protocol_level >= diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 27e635c9a..d38ba7cc6 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -833,6 +833,7 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, int rc = 0; int total, tmp; int prop_count = 0; + word32 seen_lo = 0, seen_hi = 0; /* singleton-property duplicate guard */ MqttProp* cur_prop; byte* buf = pbuf; @@ -880,6 +881,30 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf, break; } + /* [MQTT v5 2.2.2.2] Every property except User Property and + * Subscription Identifier MUST appear at most once; a duplicate is a + * Protocol Error. */ + if (cur_prop->type != MQTT_PROP_USER_PROP && + cur_prop->type != MQTT_PROP_SUBSCRIPTION_ID) { + word32 bit; + if (cur_prop->type < 32) { + bit = (word32)1 << cur_prop->type; + if (seen_lo & bit) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); + break; + } + seen_lo |= bit; + } + else { + bit = (word32)1 << (cur_prop->type - 32); + if (seen_hi & bit) { + rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY); + break; + } + seen_hi |= bit; + } + } + switch (gPropMatrix[cur_prop->type].data) { case MQTT_DATA_TYPE_BYTE: @@ -1922,6 +1947,14 @@ int MqttEncode_Publish(byte *tx_buf, int tx_buf_len, MqttPublish *publish, if (payload_len > (tx_buf_len - (header_len + variable_len))) { payload_len = (tx_buf_len - (header_len + variable_len)); } + /* Never copy more than the bytes actually present in publish->buffer. + * total_len carries the wire-declared size, which can exceed the + * decoded buffer_len; clamping here prevents an OOB read past the + * source buffer during fan-out. */ + if (publish->buffer_len > 0 && + payload_len > (int)publish->buffer_len) { + payload_len = (int)publish->buffer_len; + } if (tx_payload != NULL) { XMEMCPY(tx_payload, publish->buffer, payload_len); } @@ -3614,8 +3647,11 @@ int MqttPacket_Write(MqttClient *client, byte* tx_buf, int tx_buf_len) { int rc; #ifdef WOLFMQTT_V5 - if ((client->packet_sz_max > 0) && (tx_buf_len > - (int)client->packet_sz_max)) + /* Compare unsigned: packet_sz_max is word32 and a malicious CONNACK can + * set it above INT_MAX, where the old (int) cast turned it negative and + * wedged every write. */ + if ((client->packet_sz_max > 0) && (tx_buf_len > 0) && + ((word32)tx_buf_len > client->packet_sz_max)) { rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_SERVER_PROP); } diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index 2b234a2ea..6fb158cee 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -772,6 +772,28 @@ TEST(encode_publish_qos1_valid) ASSERT_TRUE(rc > 0); } +/* [CWE-125] The encoder must clamp the copied payload to buffer_len so a + * total_len larger than the bytes actually present cannot read past the + * source buffer (the broker fan-out OOB read). */ +TEST(encode_publish_clamps_payload_to_buffer_len) +{ + byte tx_buf[256]; + byte payload[100]; + MqttPublish pub; + int rc; + + XMEMSET(&pub, 0, sizeof(pub)); + XMEMSET(payload, 'x', sizeof(payload)); + pub.topic_name = "t"; + pub.qos = MQTT_QOS_0; + pub.buffer = payload; + pub.buffer_len = 10; /* only 10 valid bytes present */ + pub.total_len = 100; /* wire-declared size exceeds buffer_len */ + rc = MqttEncode_Publish(tx_buf, (int)sizeof(tx_buf), &pub, 0); + ASSERT_TRUE(rc > 0); + ASSERT_EQ(10, (int)pub.buffer_pos); +} + /* Verify the fixed-header flag bits (retain/QoS/dup) are actually emitted. * Covers deletion mutations of the retain / qos / duplicate branches in * MqttEncode_FixedHeader. */ @@ -1321,6 +1343,23 @@ TEST(decode_publish_v5_property_count_capped) ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); ASSERT_NULL(pub.props); } + +/* [MQTT v5 2.2.2.2] A singleton property (here TOPIC_ALIAS) appearing twice is + * a Protocol Error. Wire: PUBLISH QoS 0, topic "t", props_len=6, two + * TOPIC_ALIAS(35)=1 entries. */ +TEST(decode_publish_v5_duplicate_singleton_prop_rejected) +{ + byte buf[] = { 0x30, 0x0A, 0x00, 0x01, 't', 0x06, + 0x23, 0x00, 0x01, 0x23, 0x00, 0x01 }; + MqttPublish pub; + int rc; + + XMEMSET(&pub, 0, sizeof(pub)); + pub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub); + ASSERT_EQ(MQTT_CODE_ERROR_PROPERTY, rc); + ASSERT_NULL(pub.props); +} #endif /* WOLFMQTT_V5 */ /* [MQTT-2.3.1-1] PUBLISH with QoS > 0 must carry a non-zero Packet @@ -4751,6 +4790,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(encode_publish_qos2_packet_id_zero); RUN_TEST(encode_publish_qos0_packet_id_zero_ok); RUN_TEST(encode_publish_qos1_valid); + RUN_TEST(encode_publish_clamps_payload_to_buffer_len); RUN_TEST(encode_publish_qos1_retain_flags_in_header); RUN_TEST(encode_publish_qos2_duplicate_flags_in_header); RUN_TEST(encode_publish_qos0_no_flags_in_header); @@ -4783,6 +4823,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_publish_v5_topic_alias_zero_rejected); RUN_TEST(decode_publish_v5_response_topic_wildcard_rejected); RUN_TEST(decode_publish_v5_property_count_capped); + RUN_TEST(decode_publish_v5_duplicate_singleton_prop_rejected); #endif RUN_TEST(decode_publish_qos1_packet_id_zero_rejected); RUN_TEST(decode_publish_qos2_packet_id_zero_rejected); From 356fd47431b7027bce98805bfcb9e8824885e726 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 17:25:34 -0700 Subject: [PATCH 18/31] F-4997 F-5862 F-5863 F-5865 - Broker disconnect-with-will, pending-will re-entrancy, scoped wolfSSL cleanup, fan-out write reset --- src/mqtt_broker.c | 45 ++++++++++++++++++++++++++++++++++++++++-- src/mqtt_socket.c | 14 ++++++++++++- wolfmqtt/mqtt_client.h | 3 ++- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 8f00533c0..1dc284ce5 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -3678,9 +3678,22 @@ static int BrokerPendingWill_Process(MqttBroker* broker) if (prev) { prev->next = next; } - else { + else if (broker->pending_wills == pw) { broker->pending_wills = next; } + else { + /* The fan-out above re-entered BrokerPendingWill_Add (a WS + * close), prepending a node, so pw is no longer the head. + * Unlink pw via its real predecessor instead of clobbering the + * new head with the stale saved next. */ + BrokerPendingWill* p = broker->pending_wills; + while (p != NULL && p->next != pw) { + p = p->next; + } + if (p != NULL) { + p->next = next; + } + } if (pw->client_id) WOLFMQTT_FREE(pw->client_id); if (pw->topic) { BROKER_FORCE_ZERO(pw->topic, XSTRLEN(pw->topic) + 1); @@ -5186,6 +5199,7 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, * Sub-encoder failure is logged but not propagated. */ { int sub_rc; + int wr; MqttPublish out_pub; XMEMSET(&out_pub, 0, sizeof(out_pub)); out_pub.topic_name = topic; @@ -5214,8 +5228,15 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, (int)bc->sock, (int)sub->client->sock, BrokerLog_Sanitize(topic), eff_qos, (unsigned)pub.total_len); - (void)MqttPacket_Write(&sub->client->client, + wr = MqttPacket_Write(&sub->client->client, sub->client->tx_buf, sub_rc); + /* On a partial/non-blocking write the subscriber's + * write.pos is left mid-packet; reset it so the next + * fan-out does not resume a stale offset and desync + * this subscriber's stream. */ + if (wr != sub_rc) { + sub->client->client.write.pos = 0; + } } else { WBLOG_ERR(broker, @@ -5694,6 +5715,26 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) BrokerClient_AbnormalClose(broker, bc); return 0; } + #if defined(WOLFMQTT_V5) && defined(WOLFMQTT_BROKER_WILL) + /* [MQTT-3.14.4-3] A v5 DISCONNECT with Reason Code 0x04 + * (Disconnect with Will Message) asks the broker to publish + * the Will rather than discard it. */ + if (bc->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 && + bc->client.packet.remain_len > 0) { + MqttDisconnect disc; + XMEMSET(&disc, 0, sizeof(disc)); + disc.protocol_level = bc->protocol_level; + if (MqttDecode_Disconnect(bc->rx_buf, rc, &disc) >= 0 && + disc.reason_code == + MQTT_REASON_DISCONNECT_W_WILL_MSG) { + BrokerClient_PublishWill(broker, bc); + } + else { + BrokerClient_ClearWill(bc); + } + } + else + #endif BrokerClient_ClearWill(bc); /* normal disconnect */ /* Session persistence: keep subs if clean_session=0 */ if (bc->clean_session) { diff --git a/src/mqtt_socket.c b/src/mqtt_socket.c index b2617f4bf..e6866203c 100644 --- a/src/mqtt_socket.c +++ b/src/mqtt_socket.c @@ -435,6 +435,12 @@ int MqttSocket_Connect(MqttClient *client, const char* host, word16 port, /* Setup the WolfSSL library */ rc = wolfSSL_Init(); + if (rc == WOLFSSL_SUCCESS) { + /* Record that this client incremented wolfSSL's global init + * refcount so only it calls wolfSSL_Cleanup() on disconnect, + * never tearing down wolfSSL for sibling clients. */ + MqttClient_Flags(client, 0, MQTT_CLIENT_FLAG_WOLFSSL_INIT); + } /* Issue callback to allow setup of the wolfSSL_CTX and cert verification settings */ @@ -571,7 +577,13 @@ int MqttSocket_Disconnect(MqttClient *client) wolfSSL_CTX_free(client->tls.ctx); client->tls.ctx = NULL; } - wolfSSL_Cleanup(); + /* Only balance a wolfSSL_Init() this client actually made; otherwise a + * non-TLS disconnect could drive the global refcount to 0 and tear + * wolfSSL down for other live clients in the same process. */ + if (MqttClient_Flags(client, 0, 0) & MQTT_CLIENT_FLAG_WOLFSSL_INIT) { + wolfSSL_Cleanup(); + MqttClient_Flags(client, MQTT_CLIENT_FLAG_WOLFSSL_INIT, 0); + } #endif MqttClient_Flags(client, (MQTT_CLIENT_FLAG_IS_TLS | MQTT_CLIENT_FLAG_IS_DTLS), 0); diff --git a/wolfmqtt/mqtt_client.h b/wolfmqtt/mqtt_client.h index 416be714c..c03a04646 100644 --- a/wolfmqtt/mqtt_client.h +++ b/wolfmqtt/mqtt_client.h @@ -103,7 +103,8 @@ typedef int (*MqttPublishCb)(MqttPublish* publish); enum MqttClientFlags { MQTT_CLIENT_FLAG_IS_CONNECTED = 0x01 << 0, MQTT_CLIENT_FLAG_IS_TLS = 0x01 << 1, - MQTT_CLIENT_FLAG_IS_DTLS = 0x01 << 2 + MQTT_CLIENT_FLAG_IS_DTLS = 0x01 << 2, + MQTT_CLIENT_FLAG_WOLFSSL_INIT = 0x01 << 3 /* this client called wolfSSL_Init */ }; /*! \brief Sets flags in the MqttClient structure. To be used from the application before calling MqttClient_NetConnect. From 0154f268fd7e8791bdf05c47ded9fb2723ab5658 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 17:32:14 -0700 Subject: [PATCH 19/31] F-4777 F-4932 - Reject client PUBLISH subscription id and avoid false PUBACK on topic alloc failure --- src/mqtt_broker.c | 29 +++++++++++++++++++++--- tests/test_broker_connect.c | 44 +++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 1dc284ce5..adeb1cfd4 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -5031,6 +5031,21 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, return rc; } +#ifdef WOLFMQTT_V5 + /* [MQTT-3.3.4-6] A PUBLISH sent from a client to the server MUST NOT carry + * a Subscription Identifier; reject as a Protocol Error instead of + * forwarding the foreign id to subscribers. */ + if (bc->protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 && + pub.props != NULL && + BrokerProps_Find(pub.props, MQTT_PROP_SUBSCRIPTION_ID) != NULL) { + WBLOG_ERR(broker, "broker: client PUBLISH carried SUBSCRIPTION_ID " + "sock=%d", (int)bc->sock); + (void)BrokerSend_Disconnect(bc, MQTT_REASON_PROTOCOL_ERR); + rc = MQTT_CODE_ERROR_MALFORMED_DATA; + goto publish_cleanup; + } +#endif + /* [MQTT-3.3.2-2] PUBLISH topic name wildcard / [MQTT-4.7.3-1] * empty-topic checks now live in MqttDecode_Publish via * MqttPacket_TopicNameValid, which has already returned @@ -5116,10 +5131,18 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, topic = topic_buf; #else topic = (char*)WOLFMQTT_MALLOC(pub.topic_name_len + 1); - if (topic != NULL) { - XMEMCPY(topic, pub.topic_name, pub.topic_name_len); - topic[pub.topic_name_len] = '\0'; + if (topic == NULL) { + /* Without the topic copy, retained-store and fan-out are skipped; + * returning here prevents the QoS 1/2 ACK encoder below from + * falsely reporting SUCCESS for a message that was never + * delivered. */ + WBLOG_ERR(broker, "broker: PUBLISH topic alloc failed sock=%d", + (int)bc->sock); + rc = MQTT_CODE_ERROR_MEMORY; + goto publish_cleanup; } + XMEMCPY(topic, pub.topic_name, pub.topic_name_len); + topic[pub.topic_name_len] = '\0'; #endif } /* Use payload pointer directly from decoded packet - rx_buf is not diff --git a/tests/test_broker_connect.c b/tests/test_broker_connect.c index 98670a3f6..925d0e681 100644 --- a/tests/test_broker_connect.c +++ b/tests/test_broker_connect.c @@ -1773,6 +1773,47 @@ TEST(broker_per_client_subscription_cap) } #endif /* !WOLFMQTT_STATIC_MEMORY */ +#ifdef WOLFMQTT_V5 +/* [MQTT-3.3.4-6] A client PUBLISH carrying a Subscription Identifier is a + * Protocol Error; the broker must reject and close, not forward the foreign + * id to subscribers. */ +TEST(broker_publish_with_subscription_id_closes) +{ + MqttBroker broker; + MqttBrokerNet net; + int i; + static const byte connect[] = { + 0x10, 0x0E, + 0x00, 0x04, 'M', 'Q', 'T', 'T', + 0x05, 0x02, 0x00, 0x3C, + 0x00, /* props len = 0 */ + 0x00, 0x01, 'P' + }; + static const byte publish[] = { + 0x30, 0x06, + 0x00, 0x01, 't', + 0x02, 0x0B, 0x05 /* props: SUBSCRIPTION_ID = 5 */ + }; + + install_mock_net(&net); + XMEMSET(&broker, 0, sizeof(broker)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Init(&broker, &net)); + ASSERT_EQ(MQTT_CODE_SUCCESS, MqttBroker_Start(&broker)); + + reset_mock_clients(1); + mock_client_input_append(0, connect, sizeof(connect)); + mock_client_input_append(0, publish, sizeof(publish)); + for (i = 0; i < 16; i++) { + MqttBroker_Step(&broker); + } + + ASSERT_TRUE(g_client_closed); + + MqttBroker_Stop(&broker); + MqttBroker_Free(&broker); +} +#endif /* WOLFMQTT_V5 */ + /* [MQTT-2.3.1-1] / [MQTT-4.13]: a SUBSCRIBE packet with Packet * Identifier = 0 is malformed and the broker MUST close the connection. * MqttDecode_Subscribe returns MQTT_CODE_ERROR_PACKET_ID; this test @@ -2664,6 +2705,9 @@ int main(int argc, char** argv) #endif #ifndef WOLFMQTT_STATIC_MEMORY RUN_TEST(broker_per_client_subscription_cap); +#endif +#ifdef WOLFMQTT_V5 + RUN_TEST(broker_publish_with_subscription_id_closes); #endif RUN_TEST(broker_subscribe_packet_id_zero_closes); RUN_TEST(connack_session_present_set_on_resumed_session); From dcbc2c00d20e92e9124bd547f40f97a67f80c7cf Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 11 Jun 2026 17:48:43 -0700 Subject: [PATCH 20/31] F-4993 F-5512 - Add broker pre-CONNECT idle timeout and scrub WebSocket credential staging buffer --- src/mqtt_broker.c | 23 +++++++++++++++++++++++ wolfmqtt/mqtt_broker.h | 6 ++++++ 2 files changed, 29 insertions(+) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index adeb1cfd4..f6074818d 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -1174,8 +1174,13 @@ static int BrokerWsNetRead(void* context, byte* buf, int buf_len, if (ret < (int)ws->rx_len) { XMEMMOVE(ws->rx_buffer, ws->rx_buffer + ret, ws->rx_len - ret); ws->rx_len -= ret; + /* Scrub the vacated tail so consumed CONNECT credentials do not + * linger in the WS staging buffer. */ + BROKER_FORCE_ZERO(ws->rx_buffer + ws->rx_len, (word32)ret); } else { + /* Scrub the whole consumed buffer (may hold username/password). */ + BROKER_FORCE_ZERO(ws->rx_buffer, (word32)ws->rx_len); ws->rx_len = 0; } @@ -1299,6 +1304,8 @@ static int BrokerWsNetDisconnect(void* context) ws->tx_pending = NULL; } ws->tx_len = 0; + /* Scrub any unconsumed staged bytes (may hold credentials) before free. */ + BROKER_FORCE_ZERO(ws->rx_buffer, (word32)sizeof(ws->rx_buffer)); ws->rx_len = 0; ws->status = 0; @@ -5818,6 +5825,22 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) return 0; } } + else if (!bc->connected) { + /* Pre-CONNECT idle timeout. A freshly accepted client has + * keep_alive_sec == 0 until CONNECT completes, so it is not covered by + * the keepalive check above; evict it once it has been idle past the + * deadline (last_rx is the accept time until the first full packet) + * so half-open sockets cannot squat the client table. */ + WOLFMQTT_BROKER_TIME_T now = WOLFMQTT_BROKER_GET_TIME_S(); + if ((now - bc->last_rx) > + (WOLFMQTT_BROKER_TIME_T)BROKER_CONNECT_TIMEOUT_SEC) { + WBLOG_ERR(broker, "broker: pre-CONNECT idle timeout sock=%d", + (int)bc->sock); + BrokerSubs_RemoveClient(broker, bc); + BrokerClient_Remove(broker, bc); + return 0; + } + } return activity; } diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index dfe9a88bf..7881aab66 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -131,6 +131,12 @@ #ifndef BROKER_MAX_WILL_DELAY_SEC #define BROKER_MAX_WILL_DELAY_SEC 3600 #endif +/* Seconds a freshly accepted client has to complete a CONNECT before the + * broker evicts it, so half-open pre-CONNECT sockets cannot exhaust the client + * table (Slowloris / slot exhaustion). */ +#ifndef BROKER_CONNECT_TIMEOUT_SEC + #define BROKER_CONNECT_TIMEOUT_SEC 10 +#endif /* Maximum concurrent inbound QoS 2 packet IDs awaiting PUBREL per client. * Used to dedup duplicate PUBLISHes per [MQTT-4.3.3] (Method B). 16 covers * any reasonable client; a misbehaving client that exceeds this gets a From ab130c2131621885ab6de5c5e3a4e6a5fbaea9c7 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 09:05:59 -0700 Subject: [PATCH 21/31] Address skoll review: bound UNSUBACK reason codes, reject oversized broker publish, enforce retained cap on persist restore --- src/mqtt_broker.c | 12 ++++++++++++ src/mqtt_broker_persist.c | 7 +++++++ src/mqtt_packet.c | 8 ++++++++ tests/test_mqtt_packet.c | 19 +++++++++++++++++++ 4 files changed, 46 insertions(+) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index f6074818d..786031a88 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -5053,6 +5053,18 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, } #endif + /* The decoder only captured pub.buffer_len bytes of the payload; if that is + * short of the declared pub.total_len the message exceeded the broker + * receive buffer. Reject it rather than fanning out a packet whose + * Remaining Length overstates the bytes we actually hold. */ + if (pub.total_len > 0 && pub.buffer_len < pub.total_len) { + WBLOG_ERR(broker, + "broker: PUBLISH payload exceeds buffer (have %u of %u) sock=%d", + (unsigned)pub.buffer_len, (unsigned)pub.total_len, (int)bc->sock); + rc = MQTT_CODE_ERROR_OUT_OF_BUFFER; + goto publish_cleanup; + } + /* [MQTT-3.3.2-2] PUBLISH topic name wildcard / [MQTT-4.7.3-1] * empty-topic checks now live in MqttDecode_Publish via * MqttPacket_TopicNameValid, which has already returned diff --git a/src/mqtt_broker_persist.c b/src/mqtt_broker_persist.c index d9e21195b..6efac31ed 100644 --- a/src/mqtt_broker_persist.c +++ b/src/mqtt_broker_persist.c @@ -1311,6 +1311,12 @@ static int wmqb_decode_and_insert_retained(MqttBroker* broker, #else { BrokerRetainedMsg* m; + /* Enforce the same dynamic cap on restore that BrokerRetained_Store + * applies, otherwise a restart would reset retained_count to 0 and + * let a client add another BROKER_MAX_RETAINED topics (heap DoS). */ + if (broker->retained_count >= BROKER_MAX_RETAINED) { + return 0; + } m = (BrokerRetainedMsg*)WOLFMQTT_MALLOC(sizeof(*m)); if (m == NULL) { return MQTT_CODE_ERROR_MEMORY; @@ -1346,6 +1352,7 @@ static int wmqb_decode_and_insert_retained(MqttBroker* broker, m->expiry_sec = expiry; m->next = broker->retained; broker->retained = m; + broker->retained_count++; } #endif return 0; diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index d38ba7cc6..7506583a5 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -2945,6 +2945,14 @@ int MqttDecode_UnsubscribeAck(byte *rx_buf, int rx_buf_len, return header_len; } + /* Reject a truncated read: a broker advertising a Remaining Length larger + * than the bytes actually received would otherwise make reason_code_count + * (derived from remain_len) point past the buffer, so the caller's + * rejection scan reads out of bounds. */ + if (rx_buf_len < header_len + remain_len) { + return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER); + } + /* Validate remain_len (need at least packet_id) */ if (remain_len < MQTT_DATA_LEN_SIZE) { return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA); diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index 6fb158cee..b95551571 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -4119,6 +4119,24 @@ TEST(decode_unsuback_malformed_remain_len_one) ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc); } +/* A broker advertising a Remaining Length larger than the bytes actually + * received must be rejected, not decoded - otherwise reason_code_count would + * span past the buffer and the client's rejection scan reads out of bounds. */ +TEST(decode_unsuback_truncated_remain_len_rejected) +{ + /* Remaining Length = 10, but only 3 bytes follow the 2-byte header. */ + byte buf[] = { 0xB0, 0x0A, 0x00, 0x01, 0x00 }; + MqttUnsubscribeAck ack; + int rc; + + XMEMSET(&ack, 0, sizeof(ack)); +#ifdef WOLFMQTT_V5 + ack.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; +#endif + rc = MqttDecode_UnsubscribeAck(buf, (int)sizeof(buf), &ack); + ASSERT_EQ(MQTT_CODE_ERROR_OUT_OF_BUFFER, rc); +} + #ifdef WOLFMQTT_V5 /* A v5 UNSUBACK carries one reason code per topic filter after the * properties block. The decoder must expose the count and bytes so the @@ -4996,6 +5014,7 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_unsuback_valid); RUN_TEST(decode_unsuback_malformed_remain_len_zero); RUN_TEST(decode_unsuback_malformed_remain_len_one); + RUN_TEST(decode_unsuback_truncated_remain_len_rejected); RUN_TEST(decode_unsuback_v5_reason_codes); /* MqttDecode_Ping (PINGRESP) length validation */ From 0fb17bc111f74899fed6a8e3ca8f2f66940a00e8 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 09:15:41 -0700 Subject: [PATCH 22/31] Address skoll review: guard v5-only test registrations for non-v5 builds --- tests/test_mqtt_client.c | 2 ++ tests/test_mqtt_packet.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index cff02a95a..c35525adc 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -424,7 +424,9 @@ TEST(subscribe_broker_rejection_returns_subscribe_rejected) rc = test_init_client(); ASSERT_EQ(MQTT_CODE_SUCCESS, rc); +#ifdef WOLFMQTT_V5 test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_4; +#endif test_net.write = mock_net_write_accept; test_net.read = mock_net_read_canned; diff --git a/tests/test_mqtt_packet.c b/tests/test_mqtt_packet.c index b95551571..f3ec03fc9 100644 --- a/tests/test_mqtt_packet.c +++ b/tests/test_mqtt_packet.c @@ -5015,7 +5015,9 @@ void run_mqtt_packet_tests(void) RUN_TEST(decode_unsuback_malformed_remain_len_zero); RUN_TEST(decode_unsuback_malformed_remain_len_one); RUN_TEST(decode_unsuback_truncated_remain_len_rejected); +#ifdef WOLFMQTT_V5 RUN_TEST(decode_unsuback_v5_reason_codes); +#endif /* MqttDecode_Ping (PINGRESP) length validation */ RUN_TEST(decode_pingresp_valid); From 6bec2fb5ceafadaf3e8e88f85e5c6b8aa151f00b Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 09:27:05 -0700 Subject: [PATCH 23/31] Clarify MqttClient_Auth rx_buf scrub ordering (skoll FP: props consumed before scrub) --- src/mqtt_client.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/mqtt_client.c b/src/mqtt_client.c index 6a1aca208..cdc44c70b 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -2915,8 +2915,12 @@ int MqttClient_Auth(MqttClient *client, MqttAuth* auth) #endif /* Scrub the decoded AUTH response from rx_buf. Its properties (e.g. the - * MQTT_PROP_AUTH_DATA SASL blob) point into rx_buf and linger there until - * the next read overwrites them, so zero the buffer before returning. */ + * MQTT_PROP_AUTH_DATA SASL blob) point into rx_buf and would otherwise + * linger until the next read overwrites them. This is safe to do here: + * MqttClient_WaitType above has already run MqttClient_DecodePacket, which + * delivered auth->props to the property callback and then freed them and + * set auth->props = NULL. So the bytes are consumed before this scrub and + * the caller has no live pointer into rx_buf. */ CLIENT_FORCE_ZERO(client->rx_buf, client->rx_buf_len); /* reset state */ From 41dabde9ef9e0dde1ec35a01ceaa14958419a7a9 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 09:36:08 -0700 Subject: [PATCH 24/31] Address skoll review: free v5 DISCONNECT props, floor per-client sub cap at 1, count restore cap skips --- src/mqtt_broker.c | 4 ++++ src/mqtt_broker_persist.c | 6 ++++-- wolfmqtt/mqtt_broker.h | 8 +++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 786031a88..891b9e35d 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -5774,6 +5774,10 @@ static int BrokerClient_Process(MqttBroker* broker, BrokerClient* bc) else { BrokerClient_ClearWill(bc); } + /* Free any decoded v5 DISCONNECT properties. */ + if (disc.props != NULL) { + (void)MqttProps_Free(disc.props); + } } else #endif diff --git a/src/mqtt_broker_persist.c b/src/mqtt_broker_persist.c index 6efac31ed..502945aed 100644 --- a/src/mqtt_broker_persist.c +++ b/src/mqtt_broker_persist.c @@ -1313,9 +1313,11 @@ static int wmqb_decode_and_insert_retained(MqttBroker* broker, BrokerRetainedMsg* m; /* Enforce the same dynamic cap on restore that BrokerRetained_Store * applies, otherwise a restart would reset retained_count to 0 and - * let a client add another BROKER_MAX_RETAINED topics (heap DoS). */ + * let a client add another BROKER_MAX_RETAINED topics (heap DoS). + * Return non-zero so the iterator counts it as skipped (not loaded) + * rather than silently hiding the dropped record. */ if (broker->retained_count >= BROKER_MAX_RETAINED) { - return 0; + return 1; } m = (BrokerRetainedMsg*)WOLFMQTT_MALLOC(sizeof(*m)); if (m == NULL) { diff --git a/wolfmqtt/mqtt_broker.h b/wolfmqtt/mqtt_broker.h index 7881aab66..9f04f0d67 100644 --- a/wolfmqtt/mqtt_broker.h +++ b/wolfmqtt/mqtt_broker.h @@ -96,7 +96,13 @@ /* Per-client subscription cap so one client cannot occupy the whole shared * subscription table and deny other clients (CWE-770). */ #ifndef BROKER_MAX_SUBS_PER_CLIENT - #define BROKER_MAX_SUBS_PER_CLIENT (BROKER_MAX_SUBS / BROKER_MAX_CLIENTS) + /* At least 1 so a config where BROKER_MAX_CLIENTS > BROKER_MAX_SUBS does + * not collapse the cap to 0 and reject every subscription. */ + #if (BROKER_MAX_SUBS / BROKER_MAX_CLIENTS) > 0 + #define BROKER_MAX_SUBS_PER_CLIENT (BROKER_MAX_SUBS / BROKER_MAX_CLIENTS) + #else + #define BROKER_MAX_SUBS_PER_CLIENT 1 + #endif #endif #ifndef BROKER_MAX_CLIENT_ID_LEN #define BROKER_MAX_CLIENT_ID_LEN 64 From bd8104d5856d8e8b9a60059d59ec98bc7fa39a09 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 09:48:47 -0700 Subject: [PATCH 25/31] Address skoll review: map UNSUBSCRIBE_REJECTED string and add client unsubscribe-reject test --- src/mqtt_client.c | 2 ++ tests/test_mqtt_client.c | 43 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/mqtt_client.c b/src/mqtt_client.c index cdc44c70b..8559d87f4 100644 --- a/src/mqtt_client.c +++ b/src/mqtt_client.c @@ -3194,6 +3194,8 @@ const char* MqttClient_ReturnCodeToString(int return_code) return "Error (Broker refused connection)"; case MQTT_CODE_ERROR_SUBSCRIBE_REJECTED: return "Error (Broker rejected subscription)"; + case MQTT_CODE_ERROR_UNSUBSCRIBE_REJECTED: + return "Error (Broker rejected unsubscribe)"; #if defined(ENABLE_MQTT_CURL) case MQTT_CODE_ERROR_CURL: return "Error (libcurl)"; diff --git a/tests/test_mqtt_client.c b/tests/test_mqtt_client.c index c35525adc..e924775ad 100644 --- a/tests/test_mqtt_client.c +++ b/tests/test_mqtt_client.c @@ -452,6 +452,46 @@ TEST(subscribe_broker_rejection_returns_subscribe_rejected) subscribe.topics[0].return_code); } +#ifdef WOLFMQTT_V5 +/* A v5 UNSUBACK whose per-topic reason code has the high bit set means the + * broker refused the unsubscribe; MqttClient_Unsubscribe must surface that as + * MQTT_CODE_ERROR_UNSUBSCRIBE_REJECTED rather than success. */ +TEST(unsubscribe_broker_rejection_returns_unsubscribe_rejected) +{ + int rc; + int i; + MqttUnsubscribe unsub; + MqttTopic topics[1]; + /* v5 UNSUBACK: type=0xB0, remain=4, packet_id=43, props_len=0, + * reason=0x87 (NOT_AUTHORIZED). */ + static const byte unsuback[] = { 0xB0, 0x04, 0x00, 0x2B, 0x00, 0x87 }; + + rc = test_init_client(); + ASSERT_EQ(MQTT_CODE_SUCCESS, rc); + test_client.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5; + + test_net.write = mock_net_write_accept; + test_net.read = mock_net_read_canned; + XMEMCPY(g_canned_buf, unsuback, sizeof(unsuback)); + g_canned_len = (int)sizeof(unsuback); + g_canned_pos = 0; + + XMEMSET(&unsub, 0, sizeof(unsub)); + XMEMSET(topics, 0, sizeof(topics)); + topics[0].topic_filter = "test/topic"; + unsub.packet_id = 43; + unsub.topic_count = 1; + unsub.topics = topics; + + rc = MQTT_CODE_CONTINUE; + for (i = 0; i < 10 && rc == MQTT_CODE_CONTINUE; i++) { + rc = MqttClient_Unsubscribe(&test_client, &unsub); + } + + ASSERT_EQ(MQTT_CODE_ERROR_UNSUBSCRIBE_REJECTED, rc); +} +#endif /* WOLFMQTT_V5 */ + /* ============================================================================ * MqttClient_Disconnect Tests * ============================================================================ */ @@ -855,6 +895,9 @@ void run_mqtt_client_tests(void) RUN_TEST(subscribe_null_client); RUN_TEST(subscribe_null_subscribe); RUN_TEST(subscribe_broker_rejection_returns_subscribe_rejected); +#ifdef WOLFMQTT_V5 + RUN_TEST(unsubscribe_broker_rejection_returns_unsubscribe_rejected); +#endif /* MqttClient_Unsubscribe tests */ RUN_TEST(unsubscribe_null_client); From 7e14290e60d3ce6abd2ec2852a2384d0bcde68da Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 09:59:24 -0700 Subject: [PATCH 26/31] Address skoll review: cancel deferred retained delete on re-store and reap tombstones after delivery --- src/mqtt_broker.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 891b9e35d..4e77b975b 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -3212,6 +3212,9 @@ static int BrokerRetained_Store(MqttBroker* broker, const char* topic, while (cur) { if (cur->topic != NULL && XSTRCMP(cur->topic, topic) == 0) { msg = cur; + /* Re-publishing this topic cancels a deferred delete, otherwise a + * later delivery would reap the freshly stored message. */ + msg->pending_delete = 0; break; } cur = cur->next; @@ -3852,6 +3855,34 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, if (broker->retained_delivering > 0) { broker->retained_delivering--; } + /* When the outermost delivery finishes, reap nodes a re-entrant delete + * marked after this loop had already passed them, so they stop counting + * against BROKER_MAX_RETAINED. Safe now: no delivery loop holds pointers. */ + if (broker->retained_delivering == 0) { + BrokerRetainedMsg* p = broker->retained; + BrokerRetainedMsg* pprev = NULL; + while (p != NULL) { + BrokerRetainedMsg* pnext = p->next; + if (p->pending_delete) { + if (pprev) { + pprev->next = pnext; + } + else { + broker->retained = pnext; + } + if (p->topic) WOLFMQTT_FREE(p->topic); + if (p->payload) WOLFMQTT_FREE(p->payload); + WOLFMQTT_FREE(p); + if (broker->retained_count > 0) { + broker->retained_count--; + } + } + else { + pprev = p; + } + p = pnext; + } + } #endif } #endif /* WOLFMQTT_BROKER_RETAINED */ From abb3dccc6294500f11955067671da4af067b5309 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 10:31:49 -0700 Subject: [PATCH 27/31] Add localhost SAN to broker test certs so WebSocket TLS hostname verification passes --- scripts/broker_test/ca-cert.pem | 143 ++++++-------- scripts/broker_test/server-cert.pem | 290 +++++++++++++--------------- scripts/broker_test/server-key.pem | 50 ++--- 3 files changed, 216 insertions(+), 267 deletions(-) diff --git a/scripts/broker_test/ca-cert.pem b/scripts/broker_test/ca-cert.pem index bb4abe7bc..4a7ac6a6e 100644 --- a/scripts/broker_test/ca-cert.pem +++ b/scripts/broker_test/ca-cert.pem @@ -1,93 +1,72 @@ Certificate: Data: - Version: 3 (0x2) + Version: 1 (0x0) Serial Number: - 6b:9b:70:c6:f1:a3:94:65:19:a1:08:58:ef:a7:8d:2b:7a:83:c1:da - Signature Algorithm: sha256WithRSAEncryption - Issuer: C = US, ST = Montana, L = Bozeman, O = Sawtooth, OU = Consulting, CN = www.wolfssl.com, emailAddress = info@wolfssl.com + cb:f5:0d:cd:48:75:df:5e + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=Montana, L=Bozeman, O=Sawtooth, OU=Consulting, CN=www.wolfssl.com/emailAddress=info@wolfssl.com Validity - Not Before: Dec 18 21:25:29 2024 GMT - Not After : Sep 14 21:25:29 2027 GMT - Subject: C = US, ST = Montana, L = Bozeman, O = Sawtooth, OU = Consulting, CN = www.wolfssl.com, emailAddress = info@wolfssl.com + Not Before: Jun 12 17:30:11 2026 GMT + Not After : Jun 9 17:30:11 2036 GMT + Subject: C=US, ST=Montana, L=Bozeman, O=Sawtooth, OU=Consulting, CN=www.wolfssl.com/emailAddress=info@wolfssl.com Subject Public Key Info: Public Key Algorithm: rsaEncryption - Public-Key: (2048 bit) + RSA Public-Key: (2048 bit) Modulus: - 00:bf:0c:ca:2d:14:b2:1e:84:42:5b:cd:38:1f:4a: - f2:4d:75:10:f1:b6:35:9f:df:ca:7d:03:98:d3:ac: - de:03:66:ee:2a:f1:d8:b0:7d:6e:07:54:0b:10:98: - 21:4d:80:cb:12:20:e7:cc:4f:de:45:7d:c9:72:77: - 32:ea:ca:90:bb:69:52:10:03:2f:a8:f3:95:c5:f1: - 8b:62:56:1b:ef:67:6f:a4:10:41:95:ad:0a:9b:e3: - a5:c0:b0:d2:70:76:50:30:5b:a8:e8:08:2c:7c:ed: - a7:a2:7a:8d:38:29:1c:ac:c7:ed:f2:7c:95:b0:95: - 82:7d:49:5c:38:cd:77:25:ef:bd:80:75:53:94:3c: - 3d:ca:63:5b:9f:15:b5:d3:1d:13:2f:19:d1:3c:db: - 76:3a:cc:b8:7d:c9:e5:c2:d7:da:40:6f:d8:21:dc: - 73:1b:42:2d:53:9c:fe:1a:fc:7d:ab:7a:36:3f:98: - de:84:7c:05:67:ce:6a:14:38:87:a9:f1:8c:b5:68: - cb:68:7f:71:20:2b:f5:a0:63:f5:56:2f:a3:26:d2: - b7:6f:b1:5a:17:d7:38:99:08:fe:93:58:6f:fe:c3: - 13:49:08:16:0b:a7:4d:67:00:52:31:67:23:4e:98: - ed:51:45:1d:b9:04:d9:0b:ec:d8:28:b3:4b:bd:ed: - 36:79 + 00:ce:6b:43:26:a4:36:e0:da:0b:ee:4b:84:5f:72: + df:fe:22:76:34:44:6c:c9:45:8c:8b:be:8b:f2:af: + 67:aa:74:25:48:77:e6:44:6c:80:69:9e:ae:d2:ec: + 5c:c3:e3:f2:51:4a:6c:c9:c2:c9:cb:5e:6d:74:d9: + 28:08:e9:6d:2e:6e:5b:7b:60:b8:17:cf:90:fc:dc: + 0b:fc:8f:e8:e9:4a:6f:e8:28:7d:50:d4:f3:a1:ea: + 29:62:92:15:82:f4:3b:74:88:6d:6f:ce:a5:63:bf: + 13:fc:34:03:62:ab:c7:24:6e:4e:62:69:88:6a:7f: + af:89:78:2c:64:88:0a:6e:59:b7:a1:43:43:ce:f3: + db:7a:97:af:f2:15:d1:e2:59:ea:65:8e:94:14:92: + 16:45:d6:d0:11:c1:5d:b9:04:00:96:6b:f3:cc:e6: + bb:ab:37:f7:d8:8d:29:a1:b0:d0:cc:dd:60:50:90: + 1d:f1:8d:b0:c9:01:6a:49:d2:b0:df:79:37:3c:b0: + c5:a0:4b:54:36:a0:b0:aa:c4:a1:09:66:6a:3a:07: + a6:42:d4:83:b1:9a:30:e2:8d:fd:41:01:86:2c:6a: + bf:41:5d:d6:37:20:34:9d:2b:be:de:0f:d7:d9:4e: + fe:e0:97:53:96:02:62:f3:87:55:de:47:bc:83:26: + dd:29 Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Subject Key Identifier: - 27:8E:67:11:74:C3:26:1D:3F:ED:33:63:B3:A4:D8:1D:30:E5:E8:D5 - X509v3 Authority Key Identifier: - keyid:27:8E:67:11:74:C3:26:1D:3F:ED:33:63:B3:A4:D8:1D:30:E5:E8:D5 - DirName:/C=US/ST=Montana/L=Bozeman/O=Sawtooth/OU=Consulting/CN=www.wolfssl.com/emailAddress=info@wolfssl.com - serial:6B:9B:70:C6:F1:A3:94:65:19:A1:08:58:EF:A7:8D:2B:7A:83:C1:DA - X509v3 Basic Constraints: - CA:TRUE - X509v3 Subject Alternative Name: - DNS:example.com, IP Address:127.0.0.1 - X509v3 Extended Key Usage: - TLS Web Server Authentication, TLS Web Client Authentication Signature Algorithm: sha256WithRSAEncryption - Signature Value: - 77:3b:3d:66:74:bc:97:fe:40:16:e6:ba:a5:d5:d1:84:08:89: - 69:4f:88:0d:57:a9:ef:8c:c3:97:52:c8:bd:8b:a2:49:3b:b7: - f7:5d:1e:d6:14:7f:b2:80:33:da:a0:8a:d3:e1:2f:d5:bc:33: - 9f:ea:5a:72:24:e5:f8:b8:4b:b3:df:62:90:3b:a8:21:ef:27: - 42:75:bc:60:02:8e:37:35:99:eb:a3:28:f2:65:4c:ff:7a:f8: - 8e:cc:23:6d:e5:6a:fe:22:5a:d9:b2:4f:47:c7:e0:ae:98:ef: - 94:ac:b6:4f:61:81:29:8e:e1:79:2c:46:fc:e9:1a:c3:96:1f: - 19:93:64:2e:9f:37:72:c5:e4:93:4e:61:5f:38:8e:ae:e8:39: - 19:e6:97:a8:91:d4:23:7e:1e:d2:d0:53:ec:cc:ac:a0:1d:d0: - b7:dd:b1:b7:01:2e:96:cd:85:27:e0:e7:47:e2:c1:c1:00:f6: - 94:df:77:e7:fa:c6:ef:8a:c0:7c:67:bc:ff:a0:7c:94:3b:7d: - 86:42:af:3d:83:31:ee:2a:3b:7b:f0:2c:9e:6f:e9:c4:07:81: - 24:da:05:70:4d:dd:09:ae:9e:72:b8:21:0e:8c:b2:ab:aa:4c: - 49:10:f7:76:f9:b5:0d:6c:20:d3:df:7a:06:32:8d:29:1f:28: - 1d:8d:26:33 + 57:a9:91:4b:3c:fe:1e:b3:58:58:07:82:7f:a8:33:9b:e6:2f: + f5:e6:de:d0:72:00:1a:04:6f:15:cc:01:8b:10:1f:19:e3:d2: + 8e:d9:27:48:6c:2d:ef:c2:1e:a3:97:fe:3f:bd:cf:97:5a:1b: + 85:19:20:63:e6:2a:85:6d:08:cb:b8:38:c1:38:3f:c9:88:90: + e2:48:f4:c4:22:d2:3f:78:63:f0:a8:1b:26:08:2b:23:82:2c: + f0:e9:51:21:65:58:87:ad:73:03:41:95:67:ff:e9:b9:3d:93: + 19:49:ff:39:6a:75:c6:74:3b:e9:51:d0:07:28:14:30:f9:1a: + b2:23:63:b6:25:64:f0:3a:62:bf:84:40:65:ea:38:6c:f6:86: + 13:fb:94:98:ac:03:37:6e:e5:75:28:65:20:69:b2:f9:9a:2c: + e6:af:ad:74:78:d8:5f:f4:d2:6c:4d:32:6c:3a:ab:65:6b:d7: + 3c:f9:fb:6a:18:fb:c0:88:e4:40:0b:38:cd:d4:5a:1c:b0:bb: + 18:3a:6a:18:20:0f:27:0e:ea:38:13:50:2c:f4:3a:00:dc:e9: + 1a:d5:52:21:cb:3c:7c:e1:25:e8:d5:e1:b4:2e:79:2c:45:8a: + cb:63:90:60:57:96:a0:17:6f:d8:cf:05:76:b1:51:12:86:a3: + 58:36:00:5c -----BEGIN CERTIFICATE----- -MIIE/zCCA+egAwIBAgIUa5twxvGjlGUZoQhY76eNK3qDwdowDQYJKoZIhvcNAQEL -BQAwgZQxCzAJBgNVBAYTAlVTMRAwDgYDVQQIDAdNb250YW5hMRAwDgYDVQQHDAdC -b3plbWFuMREwDwYDVQQKDAhTYXd0b290aDETMBEGA1UECwwKQ29uc3VsdGluZzEY -MBYGA1UEAwwPd3d3LndvbGZzc2wuY29tMR8wHQYJKoZIhvcNAQkBFhBpbmZvQHdv -bGZzc2wuY29tMB4XDTI0MTIxODIxMjUyOVoXDTI3MDkxNDIxMjUyOVowgZQxCzAJ -BgNVBAYTAlVTMRAwDgYDVQQIDAdNb250YW5hMRAwDgYDVQQHDAdCb3plbWFuMREw -DwYDVQQKDAhTYXd0b290aDETMBEGA1UECwwKQ29uc3VsdGluZzEYMBYGA1UEAwwP -d3d3LndvbGZzc2wuY29tMR8wHQYJKoZIhvcNAQkBFhBpbmZvQHdvbGZzc2wuY29t -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvwzKLRSyHoRCW804H0ry -TXUQ8bY1n9/KfQOY06zeA2buKvHYsH1uB1QLEJghTYDLEiDnzE/eRX3Jcncy6sqQ -u2lSEAMvqPOVxfGLYlYb72dvpBBBla0Km+OlwLDScHZQMFuo6AgsfO2nonqNOCkc -rMft8nyVsJWCfUlcOM13Je+9gHVTlDw9ymNbnxW10x0TLxnRPNt2Osy4fcnlwtfa -QG/YIdxzG0ItU5z+Gvx9q3o2P5jehHwFZ85qFDiHqfGMtWjLaH9xICv1oGP1Vi+j -JtK3b7FaF9c4mQj+k1hv/sMTSQgWC6dNZwBSMWcjTpjtUUUduQTZC+zYKLNLve02 -eQIDAQABo4IBRTCCAUEwHQYDVR0OBBYEFCeOZxF0wyYdP+0zY7Ok2B0w5ejVMIHU -BgNVHSMEgcwwgcmAFCeOZxF0wyYdP+0zY7Ok2B0w5ejVoYGapIGXMIGUMQswCQYD -VQQGEwJVUzEQMA4GA1UECAwHTW9udGFuYTEQMA4GA1UEBwwHQm96ZW1hbjERMA8G -A1UECgwIU2F3dG9vdGgxEzARBgNVBAsMCkNvbnN1bHRpbmcxGDAWBgNVBAMMD3d3 -dy53b2xmc3NsLmNvbTEfMB0GCSqGSIb3DQEJARYQaW5mb0B3b2xmc3NsLmNvbYIU -a5twxvGjlGUZoQhY76eNK3qDwdowDAYDVR0TBAUwAwEB/zAcBgNVHREEFTATggtl -eGFtcGxlLmNvbYcEfwAAATAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw -DQYJKoZIhvcNAQELBQADggEBAHc7PWZ0vJf+QBbmuqXV0YQIiWlPiA1Xqe+Mw5dS -yL2Lokk7t/ddHtYUf7KAM9qgitPhL9W8M5/qWnIk5fi4S7PfYpA7qCHvJ0J1vGAC -jjc1meujKPJlTP96+I7MI23lav4iWtmyT0fH4K6Y75Sstk9hgSmO4XksRvzpGsOW -HxmTZC6fN3LF5JNOYV84jq7oORnml6iR1CN+HtLQU+zMrKAd0LfdsbcBLpbNhSfg -50fiwcEA9pTfd+f6xu+KwHxnvP+gfJQ7fYZCrz2DMe4qO3vwLJ5v6cQHgSTaBXBN -3QmunnK4IQ6MsquqTEkQ93b5tQ1sINPfegYyjSkfKB2NJjM= +MIIDpjCCAo4CCQDL9Q3NSHXfXjANBgkqhkiG9w0BAQsFADCBlDELMAkGA1UEBhMC +VVMxEDAOBgNVBAgMB01vbnRhbmExEDAOBgNVBAcMB0JvemVtYW4xETAPBgNVBAoM +CFNhd3Rvb3RoMRMwEQYDVQQLDApDb25zdWx0aW5nMRgwFgYDVQQDDA93d3cud29s +ZnNzbC5jb20xHzAdBgkqhkiG9w0BCQEWEGluZm9Ad29sZnNzbC5jb20wHhcNMjYw +NjEyMTczMDExWhcNMzYwNjA5MTczMDExWjCBlDELMAkGA1UEBhMCVVMxEDAOBgNV +BAgMB01vbnRhbmExEDAOBgNVBAcMB0JvemVtYW4xETAPBgNVBAoMCFNhd3Rvb3Ro +MRMwEQYDVQQLDApDb25zdWx0aW5nMRgwFgYDVQQDDA93d3cud29sZnNzbC5jb20x +HzAdBgkqhkiG9w0BCQEWEGluZm9Ad29sZnNzbC5jb20wggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDOa0MmpDbg2gvuS4Rfct/+InY0RGzJRYyLvovyr2eq +dCVId+ZEbIBpnq7S7FzD4/JRSmzJwsnLXm102SgI6W0ublt7YLgXz5D83Av8j+jp +Sm/oKH1Q1POh6ilikhWC9Dt0iG1vzqVjvxP8NANiq8ckbk5iaYhqf6+JeCxkiApu +WbehQ0PO89t6l6/yFdHiWepljpQUkhZF1tARwV25BACWa/PM5rurN/fYjSmhsNDM +3WBQkB3xjbDJAWpJ0rDfeTc8sMWgS1Q2oLCqxKEJZmo6B6ZC1IOxmjDijf1BAYYs +ar9BXdY3IDSdK77eD9fZTv7gl1OWAmLzh1XeR7yDJt0pAgMBAAEwDQYJKoZIhvcN +AQELBQADggEBAFepkUs8/h6zWFgHgn+oM5vmL/Xm3tByABoEbxXMAYsQHxnj0o7Z +J0hsLe/CHqOX/j+9z5daG4UZIGPmKoVtCMu4OME4P8mIkOJI9MQi0j94Y/CoGyYI +KyOCLPDpUSFlWIetcwNBlWf/6bk9kxlJ/zlqdcZ0O+lR0AcoFDD5GrIjY7YlZPA6 +Yr+EQGXqOGz2hhP7lJisAzdu5XUoZSBpsvmaLOavrXR42F/00mxNMmw6q2Vr1zz5 ++2oY+8CI5EALOM3UWhywuxg6ahggDycO6jgTUCz0OgDc6RrVUiHLPHzhJejV4bQu +eSxFistjkGBXlqAXb9jPBXaxURKGo1g2AFw= -----END CERTIFICATE----- diff --git a/scripts/broker_test/server-cert.pem b/scripts/broker_test/server-cert.pem index 6fc3db772..ecb280af4 100644 --- a/scripts/broker_test/server-cert.pem +++ b/scripts/broker_test/server-cert.pem @@ -1,185 +1,155 @@ Certificate: Data: Version: 3 (0x2) - Serial Number: 1 (0x1) - Signature Algorithm: sha256WithRSAEncryption - Issuer: C = US, ST = Montana, L = Bozeman, O = Sawtooth, OU = Consulting, CN = www.wolfssl.com, emailAddress = info@wolfssl.com + Serial Number: + c5:e2:b6:41:60:d5:38:e9 + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=Montana, L=Bozeman, O=Sawtooth, OU=Consulting, CN=www.wolfssl.com/emailAddress=info@wolfssl.com Validity - Not Before: Dec 18 21:25:30 2024 GMT - Not After : Sep 14 21:25:30 2027 GMT - Subject: C = US, ST = Montana, L = Bozeman, O = wolfSSL, OU = Support, CN = www.wolfssl.com, emailAddress = info@wolfssl.com + Not Before: Jun 12 17:30:11 2026 GMT + Not After : Jun 9 17:30:11 2036 GMT + Subject: C=US, ST=Montana, L=Bozeman, O=wolfSSL, OU=Support, CN=www.wolfssl.com/emailAddress=info@wolfssl.com Subject Public Key Info: Public Key Algorithm: rsaEncryption - Public-Key: (2048 bit) + RSA Public-Key: (2048 bit) Modulus: - 00:c0:95:08:e1:57:41:f2:71:6d:b7:d2:45:41:27: - 01:65:c6:45:ae:f2:bc:24:30:b8:95:ce:2f:4e:d6: - f6:1c:88:bc:7c:9f:fb:a8:67:7f:fe:5c:9c:51:75: - f7:8a:ca:07:e7:35:2f:8f:e1:bd:7b:c0:2f:7c:ab: - 64:a8:17:fc:ca:5d:7b:ba:e0:21:e5:72:2e:6f:2e: - 86:d8:95:73:da:ac:1b:53:b9:5f:3f:d7:19:0d:25: - 4f:e1:63:63:51:8b:0b:64:3f:ad:43:b8:a5:1c:5c: - 34:b3:ae:00:a0:63:c5:f6:7f:0b:59:68:78:73:a6: - 8c:18:a9:02:6d:af:c3:19:01:2e:b8:10:e3:c6:cc: - 40:b4:69:a3:46:33:69:87:6e:c4:bb:17:a6:f3:e8: - dd:ad:73:bc:7b:2f:21:b5:fd:66:51:0c:bd:54:b3: - e1:6d:5f:1c:bc:23:73:d1:09:03:89:14:d2:10:b9: - 64:c3:2a:d0:a1:96:4a:bc:e1:d4:1a:5b:c7:a0:c0: - c1:63:78:0f:44:37:30:32:96:80:32:23:95:a1:77: - ba:13:d2:97:73:e2:5d:25:c9:6a:0d:c3:39:60:a4: - b4:b0:69:42:42:09:e9:d8:08:bc:33:20:b3:58:22: - a7:aa:eb:c4:e1:e6:61:83:c5:d2:96:df:d9:d0:4f: - ad:d7 + 00:ca:2c:8c:fc:f7:ae:fb:c1:4b:e9:c6:1a:38:52: + e7:07:f8:ec:9b:63:6d:87:9e:32:b4:bd:22:cb:9c: + ae:a7:64:65:d3:42:c5:6a:49:58:7b:be:5c:a8:d7: + 0a:3d:93:a8:27:fd:aa:eb:16:46:8e:61:08:ca:45: + eb:0b:e7:54:bc:d9:20:69:6b:33:c6:e6:73:f9:40: + cc:76:34:3b:7a:c6:5f:26:29:56:a8:82:4d:2d:78: + 2d:53:07:f0:4e:f5:01:b6:48:54:e4:88:af:2d:75: + 39:bd:b6:71:60:2b:52:3b:44:d5:dc:8d:77:56:0b: + 9e:b2:7a:a3:c8:dd:48:50:64:93:5d:ce:c8:d0:08: + e4:4a:6b:f8:2b:02:6b:c4:af:11:44:c2:9b:a3:40: + 2b:2b:a2:f3:74:05:08:7c:0d:74:90:4c:5e:33:f7: + 1b:32:1f:08:03:aa:9c:93:67:8b:86:1c:54:f1:8a: + e9:a7:f7:67:7d:6e:4b:8e:23:01:7d:0e:ba:53:54: + 00:ce:3b:34:1d:5c:f4:10:bd:8f:c0:3f:85:1c:18: + 68:80:ee:78:8b:2f:50:47:f6:8a:8d:9d:24:50:ef: + c5:fc:f1:f7:1e:be:28:ce:98:8f:61:06:5c:c1:e9: + 94:f4:0c:f2:10:71:9e:23:6c:5b:8a:7d:cc:03:ba: + b9:2f Exponent: 65537 (0x10001) X509v3 extensions: - X509v3 Subject Key Identifier: - B3:11:32:C9:92:98:84:E2:C9:F8:D0:3B:6E:03:42:CA:1F:0E:8E:3C - X509v3 Authority Key Identifier: - keyid:27:8E:67:11:74:C3:26:1D:3F:ED:33:63:B3:A4:D8:1D:30:E5:E8:D5 - DirName:/C=US/ST=Montana/L=Bozeman/O=Sawtooth/OU=Consulting/CN=www.wolfssl.com/emailAddress=info@wolfssl.com - serial:6B:9B:70:C6:F1:A3:94:65:19:A1:08:58:EF:A7:8D:2B:7A:83:C1:DA - X509v3 Basic Constraints: - CA:TRUE X509v3 Subject Alternative Name: - DNS:example.com, IP Address:127.0.0.1 + DNS:localhost, DNS:example.com, IP Address:127.0.0.1 + X509v3 Basic Constraints: + CA:FALSE + X509v3 Key Usage: + Digital Signature, Key Encipherment X509v3 Extended Key Usage: - TLS Web Server Authentication, TLS Web Client Authentication + TLS Web Server Authentication Signature Algorithm: sha256WithRSAEncryption - Signature Value: - 8a:f1:4e:e8:9f:59:b2:d9:13:ac:fc:42:c4:81:34:9f:6b:39: - 57:9c:e9:92:5d:41:ac:05:35:b1:26:93:4d:4a:da:f8:51:82: - d2:8d:7f:d3:5c:6e:29:80:8d:9b:02:10:2b:64:f5:d1:31:06: - fa:85:2b:8f:63:32:14:76:7a:39:15:f3:4e:dd:fd:e2:2c:90: - 15:d1:6f:73:87:ee:e6:c8:eb:ad:40:d5:e8:94:1f:a6:7e:26: - 5b:87:ba:0f:06:5a:4d:55:7a:aa:c4:09:34:8b:f7:e5:cc:d6: - b7:6c:46:6d:a1:e6:66:66:4c:4b:e5:12:31:37:54:49:64:a5: - 66:eb:e0:c6:a1:49:f8:4d:c3:d3:55:a4:05:d2:ac:fb:e1:c8: - 69:30:4b:98:fd:72:1a:ab:9f:86:eb:0d:bd:7c:a6:3d:81:d9: - 01:a7:8a:79:ab:3c:ce:e5:b6:c3:1b:ef:7d:5e:37:7b:37:7c: - 91:89:59:11:21:11:7c:05:80:e1:a8:d6:f9:35:da:1b:86:06: - 5a:32:67:6c:a9:2b:e0:31:7b:89:53:37:42:af:34:a4:53:d2: - 7c:91:50:63:3a:8e:4a:1f:a3:90:4e:7c:41:59:1d:eb:7b:a2: - 14:87:ba:76:36:a4:77:46:34:f2:55:50:f0:24:9f:83:83:da: - a6:aa:3c:c8 + b0:14:09:45:a7:16:c2:fd:01:fa:a5:ca:dd:14:3b:5d:0d:80: + a2:d2:d6:ef:7c:bd:c1:49:98:05:35:52:6e:67:c7:9f:23:f2: + ad:b1:4c:0e:50:b2:c7:b0:0e:b6:44:42:5b:fe:21:29:71:6c: + 54:36:a9:a3:31:c6:1f:04:ee:a3:50:b5:fd:33:f9:d2:c4:58: + 11:e1:4d:c3:c3:b3:1f:db:42:95:11:ac:e6:f8:33:d5:46:97: + 2c:47:db:97:53:f1:7b:3f:14:bb:8a:b1:2f:f6:0a:11:82:8c: + ce:45:3a:25:91:a3:73:3a:95:90:89:d2:b2:cc:91:8c:d8:2a: + ff:3b:3d:40:ca:6d:d9:5d:05:7e:52:88:8b:b9:2b:a2:52:be: + 42:0a:09:a8:ba:2b:e3:5c:a6:f3:89:4a:50:40:e3:1a:a6:24: + 2f:b6:de:ce:7d:72:6c:fa:d3:2f:52:0f:16:00:9c:95:bf:54: + 20:1e:ab:99:25:66:61:f9:d5:1e:8b:0d:35:57:8b:d4:5e:16: + c8:3d:f3:7d:a4:2f:2e:55:1d:3f:e8:01:e7:eb:17:37:0c:0d: + ba:1e:9e:a7:33:9e:a9:90:12:36:4c:62:63:e5:02:f2:aa:33: + a6:99:bf:89:02:98:7d:cf:ff:cd:0f:f9:54:9a:9d:e7:83:dd: + 0b:8c:11:39 -----BEGIN CERTIFICATE----- -MIIE6DCCA9CgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBlDELMAkGA1UEBhMCVVMx -EDAOBgNVBAgMB01vbnRhbmExEDAOBgNVBAcMB0JvemVtYW4xETAPBgNVBAoMCFNh -d3Rvb3RoMRMwEQYDVQQLDApDb25zdWx0aW5nMRgwFgYDVQQDDA93d3cud29sZnNz -bC5jb20xHzAdBgkqhkiG9w0BCQEWEGluZm9Ad29sZnNzbC5jb20wHhcNMjQxMjE4 -MjEyNTMwWhcNMjcwOTE0MjEyNTMwWjCBkDELMAkGA1UEBhMCVVMxEDAOBgNVBAgM -B01vbnRhbmExEDAOBgNVBAcMB0JvemVtYW4xEDAOBgNVBAoMB3dvbGZTU0wxEDAO -BgNVBAsMB1N1cHBvcnQxGDAWBgNVBAMMD3d3dy53b2xmc3NsLmNvbTEfMB0GCSqG -SIb3DQEJARYQaW5mb0B3b2xmc3NsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAMCVCOFXQfJxbbfSRUEnAWXGRa7yvCQwuJXOL07W9hyIvHyf+6hn -f/5cnFF194rKB+c1L4/hvXvAL3yrZKgX/Mpde7rgIeVyLm8uhtiVc9qsG1O5Xz/X -GQ0lT+FjY1GLC2Q/rUO4pRxcNLOuAKBjxfZ/C1loeHOmjBipAm2vwxkBLrgQ48bM -QLRpo0YzaYduxLsXpvPo3a1zvHsvIbX9ZlEMvVSz4W1fHLwjc9EJA4kU0hC5ZMMq -0KGWSrzh1Bpbx6DAwWN4D0Q3MDKWgDIjlaF3uhPSl3PiXSXJag3DOWCktLBpQkIJ -6dgIvDMgs1gip6rrxOHmYYPF0pbf2dBPrdcCAwEAAaOCAUUwggFBMB0GA1UdDgQW -BBSzETLJkpiE4sn40DtuA0LKHw6OPDCB1AYDVR0jBIHMMIHJgBQnjmcRdMMmHT/t -M2OzpNgdMOXo1aGBmqSBlzCBlDELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB01vbnRh -bmExEDAOBgNVBAcMB0JvemVtYW4xETAPBgNVBAoMCFNhd3Rvb3RoMRMwEQYDVQQL -DApDb25zdWx0aW5nMRgwFgYDVQQDDA93d3cud29sZnNzbC5jb20xHzAdBgkqhkiG -9w0BCQEWEGluZm9Ad29sZnNzbC5jb22CFGubcMbxo5RlGaEIWO+njSt6g8HaMAwG -A1UdEwQFMAMBAf8wHAYDVR0RBBUwE4ILZXhhbXBsZS5jb22HBH8AAAEwHQYDVR0l -BBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA0GCSqGSIb3DQEBCwUAA4IBAQCK8U7o -n1my2ROs/ELEgTSfazlXnOmSXUGsBTWxJpNNStr4UYLSjX/TXG4pgI2bAhArZPXR -MQb6hSuPYzIUdno5FfNO3f3iLJAV0W9zh+7myOutQNXolB+mfiZbh7oPBlpNVXqq -xAk0i/flzNa3bEZtoeZmZkxL5RIxN1RJZKVm6+DGoUn4TcPTVaQF0qz74chpMEuY -/XIaq5+G6w29fKY9gdkBp4p5qzzO5bbDG+99Xjd7N3yRiVkRIRF8BYDhqNb5Ndob -hgZaMmdsqSvgMXuJUzdCrzSkU9J8kVBjOo5KH6OQTnxBWR3re6IUh7p2NqR3RjTy -VVDwJJ+Dg9qmqjzI +MIIEATCCAumgAwIBAgIJAMXitkFg1TjpMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYD +VQQGEwJVUzEQMA4GA1UECAwHTW9udGFuYTEQMA4GA1UEBwwHQm96ZW1hbjERMA8G +A1UECgwIU2F3dG9vdGgxEzARBgNVBAsMCkNvbnN1bHRpbmcxGDAWBgNVBAMMD3d3 +dy53b2xmc3NsLmNvbTEfMB0GCSqGSIb3DQEJARYQaW5mb0B3b2xmc3NsLmNvbTAe +Fw0yNjA2MTIxNzMwMTFaFw0zNjA2MDkxNzMwMTFaMIGQMQswCQYDVQQGEwJVUzEQ +MA4GA1UECAwHTW9udGFuYTEQMA4GA1UEBwwHQm96ZW1hbjEQMA4GA1UECgwHd29s +ZlNTTDEQMA4GA1UECwwHU3VwcG9ydDEYMBYGA1UEAwwPd3d3LndvbGZzc2wuY29t +MR8wHQYJKoZIhvcNAQkBFhBpbmZvQHdvbGZzc2wuY29tMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAyiyM/Peu+8FL6cYaOFLnB/jsm2Nth54ytL0iy5yu +p2Rl00LFaklYe75cqNcKPZOoJ/2q6xZGjmEIykXrC+dUvNkgaWszxuZz+UDMdjQ7 +esZfJilWqIJNLXgtUwfwTvUBtkhU5IivLXU5vbZxYCtSO0TV3I13VguesnqjyN1I +UGSTXc7I0AjkSmv4KwJrxK8RRMKbo0ArK6LzdAUIfA10kExeM/cbMh8IA6qck2eL +hhxU8Yrpp/dnfW5LjiMBfQ66U1QAzjs0HVz0EL2PwD+FHBhogO54iy9QR/aKjZ0k +UO/F/PH3Hr4ozpiPYQZcwemU9AzyEHGeI2xbin3MA7q5LwIDAQABo1gwVjAnBgNV +HREEIDAegglsb2NhbGhvc3SCC2V4YW1wbGUuY29thwR/AAABMAkGA1UdEwQCMAAw +CwYDVR0PBAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0GCSqGSIb3DQEBCwUA +A4IBAQCwFAlFpxbC/QH6pcrdFDtdDYCi0tbvfL3BSZgFNVJuZ8efI/KtsUwOULLH +sA62REJb/iEpcWxUNqmjMcYfBO6jULX9M/nSxFgR4U3Dw7Mf20KVEazm+DPVRpcs +R9uXU/F7PxS7irEv9goRgozORTolkaNzOpWQidKyzJGM2Cr/Oz1Aym3ZXQV+UoiL +uSuiUr5CCgmouivjXKbziUpQQOMapiQvtt7OfXJs+tMvUg8WAJyVv1QgHquZJWZh ++dUeiw01V4vUXhbIPfN9pC8uVR0/6AHn6xc3DA26Hp6nM56pkBI2TGJj5QLyqjOm +mb+JAph9z//ND/lUmp3ng90LjBE5 -----END CERTIFICATE----- Certificate: Data: - Version: 3 (0x2) + Version: 1 (0x0) Serial Number: - 6b:9b:70:c6:f1:a3:94:65:19:a1:08:58:ef:a7:8d:2b:7a:83:c1:da - Signature Algorithm: sha256WithRSAEncryption - Issuer: C = US, ST = Montana, L = Bozeman, O = Sawtooth, OU = Consulting, CN = www.wolfssl.com, emailAddress = info@wolfssl.com + cb:f5:0d:cd:48:75:df:5e + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=Montana, L=Bozeman, O=Sawtooth, OU=Consulting, CN=www.wolfssl.com/emailAddress=info@wolfssl.com Validity - Not Before: Dec 18 21:25:29 2024 GMT - Not After : Sep 14 21:25:29 2027 GMT - Subject: C = US, ST = Montana, L = Bozeman, O = Sawtooth, OU = Consulting, CN = www.wolfssl.com, emailAddress = info@wolfssl.com + Not Before: Jun 12 17:30:11 2026 GMT + Not After : Jun 9 17:30:11 2036 GMT + Subject: C=US, ST=Montana, L=Bozeman, O=Sawtooth, OU=Consulting, CN=www.wolfssl.com/emailAddress=info@wolfssl.com Subject Public Key Info: Public Key Algorithm: rsaEncryption - Public-Key: (2048 bit) + RSA Public-Key: (2048 bit) Modulus: - 00:bf:0c:ca:2d:14:b2:1e:84:42:5b:cd:38:1f:4a: - f2:4d:75:10:f1:b6:35:9f:df:ca:7d:03:98:d3:ac: - de:03:66:ee:2a:f1:d8:b0:7d:6e:07:54:0b:10:98: - 21:4d:80:cb:12:20:e7:cc:4f:de:45:7d:c9:72:77: - 32:ea:ca:90:bb:69:52:10:03:2f:a8:f3:95:c5:f1: - 8b:62:56:1b:ef:67:6f:a4:10:41:95:ad:0a:9b:e3: - a5:c0:b0:d2:70:76:50:30:5b:a8:e8:08:2c:7c:ed: - a7:a2:7a:8d:38:29:1c:ac:c7:ed:f2:7c:95:b0:95: - 82:7d:49:5c:38:cd:77:25:ef:bd:80:75:53:94:3c: - 3d:ca:63:5b:9f:15:b5:d3:1d:13:2f:19:d1:3c:db: - 76:3a:cc:b8:7d:c9:e5:c2:d7:da:40:6f:d8:21:dc: - 73:1b:42:2d:53:9c:fe:1a:fc:7d:ab:7a:36:3f:98: - de:84:7c:05:67:ce:6a:14:38:87:a9:f1:8c:b5:68: - cb:68:7f:71:20:2b:f5:a0:63:f5:56:2f:a3:26:d2: - b7:6f:b1:5a:17:d7:38:99:08:fe:93:58:6f:fe:c3: - 13:49:08:16:0b:a7:4d:67:00:52:31:67:23:4e:98: - ed:51:45:1d:b9:04:d9:0b:ec:d8:28:b3:4b:bd:ed: - 36:79 + 00:ce:6b:43:26:a4:36:e0:da:0b:ee:4b:84:5f:72: + df:fe:22:76:34:44:6c:c9:45:8c:8b:be:8b:f2:af: + 67:aa:74:25:48:77:e6:44:6c:80:69:9e:ae:d2:ec: + 5c:c3:e3:f2:51:4a:6c:c9:c2:c9:cb:5e:6d:74:d9: + 28:08:e9:6d:2e:6e:5b:7b:60:b8:17:cf:90:fc:dc: + 0b:fc:8f:e8:e9:4a:6f:e8:28:7d:50:d4:f3:a1:ea: + 29:62:92:15:82:f4:3b:74:88:6d:6f:ce:a5:63:bf: + 13:fc:34:03:62:ab:c7:24:6e:4e:62:69:88:6a:7f: + af:89:78:2c:64:88:0a:6e:59:b7:a1:43:43:ce:f3: + db:7a:97:af:f2:15:d1:e2:59:ea:65:8e:94:14:92: + 16:45:d6:d0:11:c1:5d:b9:04:00:96:6b:f3:cc:e6: + bb:ab:37:f7:d8:8d:29:a1:b0:d0:cc:dd:60:50:90: + 1d:f1:8d:b0:c9:01:6a:49:d2:b0:df:79:37:3c:b0: + c5:a0:4b:54:36:a0:b0:aa:c4:a1:09:66:6a:3a:07: + a6:42:d4:83:b1:9a:30:e2:8d:fd:41:01:86:2c:6a: + bf:41:5d:d6:37:20:34:9d:2b:be:de:0f:d7:d9:4e: + fe:e0:97:53:96:02:62:f3:87:55:de:47:bc:83:26: + dd:29 Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Subject Key Identifier: - 27:8E:67:11:74:C3:26:1D:3F:ED:33:63:B3:A4:D8:1D:30:E5:E8:D5 - X509v3 Authority Key Identifier: - keyid:27:8E:67:11:74:C3:26:1D:3F:ED:33:63:B3:A4:D8:1D:30:E5:E8:D5 - DirName:/C=US/ST=Montana/L=Bozeman/O=Sawtooth/OU=Consulting/CN=www.wolfssl.com/emailAddress=info@wolfssl.com - serial:6B:9B:70:C6:F1:A3:94:65:19:A1:08:58:EF:A7:8D:2B:7A:83:C1:DA - X509v3 Basic Constraints: - CA:TRUE - X509v3 Subject Alternative Name: - DNS:example.com, IP Address:127.0.0.1 - X509v3 Extended Key Usage: - TLS Web Server Authentication, TLS Web Client Authentication Signature Algorithm: sha256WithRSAEncryption - Signature Value: - 77:3b:3d:66:74:bc:97:fe:40:16:e6:ba:a5:d5:d1:84:08:89: - 69:4f:88:0d:57:a9:ef:8c:c3:97:52:c8:bd:8b:a2:49:3b:b7: - f7:5d:1e:d6:14:7f:b2:80:33:da:a0:8a:d3:e1:2f:d5:bc:33: - 9f:ea:5a:72:24:e5:f8:b8:4b:b3:df:62:90:3b:a8:21:ef:27: - 42:75:bc:60:02:8e:37:35:99:eb:a3:28:f2:65:4c:ff:7a:f8: - 8e:cc:23:6d:e5:6a:fe:22:5a:d9:b2:4f:47:c7:e0:ae:98:ef: - 94:ac:b6:4f:61:81:29:8e:e1:79:2c:46:fc:e9:1a:c3:96:1f: - 19:93:64:2e:9f:37:72:c5:e4:93:4e:61:5f:38:8e:ae:e8:39: - 19:e6:97:a8:91:d4:23:7e:1e:d2:d0:53:ec:cc:ac:a0:1d:d0: - b7:dd:b1:b7:01:2e:96:cd:85:27:e0:e7:47:e2:c1:c1:00:f6: - 94:df:77:e7:fa:c6:ef:8a:c0:7c:67:bc:ff:a0:7c:94:3b:7d: - 86:42:af:3d:83:31:ee:2a:3b:7b:f0:2c:9e:6f:e9:c4:07:81: - 24:da:05:70:4d:dd:09:ae:9e:72:b8:21:0e:8c:b2:ab:aa:4c: - 49:10:f7:76:f9:b5:0d:6c:20:d3:df:7a:06:32:8d:29:1f:28: - 1d:8d:26:33 + 57:a9:91:4b:3c:fe:1e:b3:58:58:07:82:7f:a8:33:9b:e6:2f: + f5:e6:de:d0:72:00:1a:04:6f:15:cc:01:8b:10:1f:19:e3:d2: + 8e:d9:27:48:6c:2d:ef:c2:1e:a3:97:fe:3f:bd:cf:97:5a:1b: + 85:19:20:63:e6:2a:85:6d:08:cb:b8:38:c1:38:3f:c9:88:90: + e2:48:f4:c4:22:d2:3f:78:63:f0:a8:1b:26:08:2b:23:82:2c: + f0:e9:51:21:65:58:87:ad:73:03:41:95:67:ff:e9:b9:3d:93: + 19:49:ff:39:6a:75:c6:74:3b:e9:51:d0:07:28:14:30:f9:1a: + b2:23:63:b6:25:64:f0:3a:62:bf:84:40:65:ea:38:6c:f6:86: + 13:fb:94:98:ac:03:37:6e:e5:75:28:65:20:69:b2:f9:9a:2c: + e6:af:ad:74:78:d8:5f:f4:d2:6c:4d:32:6c:3a:ab:65:6b:d7: + 3c:f9:fb:6a:18:fb:c0:88:e4:40:0b:38:cd:d4:5a:1c:b0:bb: + 18:3a:6a:18:20:0f:27:0e:ea:38:13:50:2c:f4:3a:00:dc:e9: + 1a:d5:52:21:cb:3c:7c:e1:25:e8:d5:e1:b4:2e:79:2c:45:8a: + cb:63:90:60:57:96:a0:17:6f:d8:cf:05:76:b1:51:12:86:a3: + 58:36:00:5c -----BEGIN CERTIFICATE----- -MIIE/zCCA+egAwIBAgIUa5twxvGjlGUZoQhY76eNK3qDwdowDQYJKoZIhvcNAQEL -BQAwgZQxCzAJBgNVBAYTAlVTMRAwDgYDVQQIDAdNb250YW5hMRAwDgYDVQQHDAdC -b3plbWFuMREwDwYDVQQKDAhTYXd0b290aDETMBEGA1UECwwKQ29uc3VsdGluZzEY -MBYGA1UEAwwPd3d3LndvbGZzc2wuY29tMR8wHQYJKoZIhvcNAQkBFhBpbmZvQHdv -bGZzc2wuY29tMB4XDTI0MTIxODIxMjUyOVoXDTI3MDkxNDIxMjUyOVowgZQxCzAJ -BgNVBAYTAlVTMRAwDgYDVQQIDAdNb250YW5hMRAwDgYDVQQHDAdCb3plbWFuMREw -DwYDVQQKDAhTYXd0b290aDETMBEGA1UECwwKQ29uc3VsdGluZzEYMBYGA1UEAwwP -d3d3LndvbGZzc2wuY29tMR8wHQYJKoZIhvcNAQkBFhBpbmZvQHdvbGZzc2wuY29t -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvwzKLRSyHoRCW804H0ry -TXUQ8bY1n9/KfQOY06zeA2buKvHYsH1uB1QLEJghTYDLEiDnzE/eRX3Jcncy6sqQ -u2lSEAMvqPOVxfGLYlYb72dvpBBBla0Km+OlwLDScHZQMFuo6AgsfO2nonqNOCkc -rMft8nyVsJWCfUlcOM13Je+9gHVTlDw9ymNbnxW10x0TLxnRPNt2Osy4fcnlwtfa -QG/YIdxzG0ItU5z+Gvx9q3o2P5jehHwFZ85qFDiHqfGMtWjLaH9xICv1oGP1Vi+j -JtK3b7FaF9c4mQj+k1hv/sMTSQgWC6dNZwBSMWcjTpjtUUUduQTZC+zYKLNLve02 -eQIDAQABo4IBRTCCAUEwHQYDVR0OBBYEFCeOZxF0wyYdP+0zY7Ok2B0w5ejVMIHU -BgNVHSMEgcwwgcmAFCeOZxF0wyYdP+0zY7Ok2B0w5ejVoYGapIGXMIGUMQswCQYD -VQQGEwJVUzEQMA4GA1UECAwHTW9udGFuYTEQMA4GA1UEBwwHQm96ZW1hbjERMA8G -A1UECgwIU2F3dG9vdGgxEzARBgNVBAsMCkNvbnN1bHRpbmcxGDAWBgNVBAMMD3d3 -dy53b2xmc3NsLmNvbTEfMB0GCSqGSIb3DQEJARYQaW5mb0B3b2xmc3NsLmNvbYIU -a5twxvGjlGUZoQhY76eNK3qDwdowDAYDVR0TBAUwAwEB/zAcBgNVHREEFTATggtl -eGFtcGxlLmNvbYcEfwAAATAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw -DQYJKoZIhvcNAQELBQADggEBAHc7PWZ0vJf+QBbmuqXV0YQIiWlPiA1Xqe+Mw5dS -yL2Lokk7t/ddHtYUf7KAM9qgitPhL9W8M5/qWnIk5fi4S7PfYpA7qCHvJ0J1vGAC -jjc1meujKPJlTP96+I7MI23lav4iWtmyT0fH4K6Y75Sstk9hgSmO4XksRvzpGsOW -HxmTZC6fN3LF5JNOYV84jq7oORnml6iR1CN+HtLQU+zMrKAd0LfdsbcBLpbNhSfg -50fiwcEA9pTfd+f6xu+KwHxnvP+gfJQ7fYZCrz2DMe4qO3vwLJ5v6cQHgSTaBXBN -3QmunnK4IQ6MsquqTEkQ93b5tQ1sINPfegYyjSkfKB2NJjM= +MIIDpjCCAo4CCQDL9Q3NSHXfXjANBgkqhkiG9w0BAQsFADCBlDELMAkGA1UEBhMC +VVMxEDAOBgNVBAgMB01vbnRhbmExEDAOBgNVBAcMB0JvemVtYW4xETAPBgNVBAoM +CFNhd3Rvb3RoMRMwEQYDVQQLDApDb25zdWx0aW5nMRgwFgYDVQQDDA93d3cud29s +ZnNzbC5jb20xHzAdBgkqhkiG9w0BCQEWEGluZm9Ad29sZnNzbC5jb20wHhcNMjYw +NjEyMTczMDExWhcNMzYwNjA5MTczMDExWjCBlDELMAkGA1UEBhMCVVMxEDAOBgNV +BAgMB01vbnRhbmExEDAOBgNVBAcMB0JvemVtYW4xETAPBgNVBAoMCFNhd3Rvb3Ro +MRMwEQYDVQQLDApDb25zdWx0aW5nMRgwFgYDVQQDDA93d3cud29sZnNzbC5jb20x +HzAdBgkqhkiG9w0BCQEWEGluZm9Ad29sZnNzbC5jb20wggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDOa0MmpDbg2gvuS4Rfct/+InY0RGzJRYyLvovyr2eq +dCVId+ZEbIBpnq7S7FzD4/JRSmzJwsnLXm102SgI6W0ublt7YLgXz5D83Av8j+jp +Sm/oKH1Q1POh6ilikhWC9Dt0iG1vzqVjvxP8NANiq8ckbk5iaYhqf6+JeCxkiApu +WbehQ0PO89t6l6/yFdHiWepljpQUkhZF1tARwV25BACWa/PM5rurN/fYjSmhsNDM +3WBQkB3xjbDJAWpJ0rDfeTc8sMWgS1Q2oLCqxKEJZmo6B6ZC1IOxmjDijf1BAYYs +ar9BXdY3IDSdK77eD9fZTv7gl1OWAmLzh1XeR7yDJt0pAgMBAAEwDQYJKoZIhvcN +AQELBQADggEBAFepkUs8/h6zWFgHgn+oM5vmL/Xm3tByABoEbxXMAYsQHxnj0o7Z +J0hsLe/CHqOX/j+9z5daG4UZIGPmKoVtCMu4OME4P8mIkOJI9MQi0j94Y/CoGyYI +KyOCLPDpUSFlWIetcwNBlWf/6bk9kxlJ/zlqdcZ0O+lR0AcoFDD5GrIjY7YlZPA6 +Yr+EQGXqOGz2hhP7lJisAzdu5XUoZSBpsvmaLOavrXR42F/00mxNMmw6q2Vr1zz5 ++2oY+8CI5EALOM3UWhywuxg6ahggDycO6jgTUCz0OgDc6RrVUiHLPHzhJejV4bQu +eSxFistjkGBXlqAXb9jPBXaxURKGo1g2AFw= -----END CERTIFICATE----- diff --git a/scripts/broker_test/server-key.pem b/scripts/broker_test/server-key.pem index d1627f4d4..ede188463 100644 --- a/scripts/broker_test/server-key.pem +++ b/scripts/broker_test/server-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEAwJUI4VdB8nFtt9JFQScBZcZFrvK8JDC4lc4vTtb2HIi8fJ/7 -qGd//lycUXX3isoH5zUvj+G9e8AvfKtkqBf8yl17uuAh5XIuby6G2JVz2qwbU7lf -P9cZDSVP4WNjUYsLZD+tQ7ilHFw0s64AoGPF9n8LWWh4c6aMGKkCba/DGQEuuBDj -xsxAtGmjRjNph27Euxem8+jdrXO8ey8htf1mUQy9VLPhbV8cvCNz0QkDiRTSELlk -wyrQoZZKvOHUGlvHoMDBY3gPRDcwMpaAMiOVoXe6E9KXc+JdJclqDcM5YKS0sGlC -Qgnp2Ai8MyCzWCKnquvE4eZhg8XSlt/Z0E+t1wIDAQABAoIBAQCa0DQPUmIFUAHv -n+1kbsLE2hryhNeSEEiSxOlq64t1bMZ5OPLJckqGZFSVd8vDmp231B2kAMieTuTd -x7pnFsF0vKnWlI8rMBr77d8hBSPZSjm9mGtlmrjcxH3upkMVLj2+HSJgKnMw1T7Y -oqyGQy7E9WReP4l1DxHYUSVOn9iqo85gs+KK2X4b8GTKmlsFC1uqy+XjP24yIgXz -0PrvdFKB4l90073/MYNFdfpjepcu1rYZxpIm5CgGUFAOeC6peA0Ul7QS2DFAq6EB -QcIw+AdfFuRhd9Jg8p+N6PS662PeKpeB70xs5lU0USsoNPRTHMRYCj+7r7X3SoVD -LTzxWFiBAoGBAPIsVHY5I2PJEDK3k62vvhl1loFk5rW4iUJB0W3QHBv4G6xpyzY8 -ZH3c9Bm4w2CxV0hfUk9ZOlV/MsAZQ1A/rs5vF/MOn0DKTq0VO8l56cBZOHNwnAp8 -yTpIMqfYSXUKhcLC/RVz2pkJKmmanwpxv7AEpox6Wm9IWlQ7xrFTF9/nAoGBAMuT -3ncVXbdcXHzYkKmYLdZpDmOzo9ymzItqpKISjI57SCyySzfcBhh96v52odSh6T8N -zRtfr1+elltbD6F8r7ObkNtXczrtsCNErkFPHwdCEyNMy/r0FKTV9542fFufqDzB -hV900jkt/9CE3/uzIHoumxeu5roLrl9TpFLtG8SRAoGBAOyY2rvV/vlSSn0CVUlv -VW5SL4SjK7OGYrNU0mNS2uOIdqDvixWl0xgUcndex6MEH54ZYrUbG57D8rUy+UzB -qusMJn3UX0pRXKRFBnBEp1bA1CIUdp7YY1CJkNPiv4GVkjFBhzkaQwsYpVMfORpf -H0O8h2rfbtMiAP4imHBOGhkpAoGBAIpBVihRnl/Ungs7mKNU8mxW1KrpaTOFJAza -1AwtxL9PAmk4fNTm3Ezt1xYRwz4A58MmwFEC3rt1nG9WnHrzju/PisUr0toGakTJ -c/5umYf4W77xfOZltU9s8MnF/xbKixsX4lg9ojerAby/QM5TjI7t7+5ZneBj5nxe -9Y5L8TvBAoGATUX5QIzFW/QqGoq08hysa+kMVja3TnKW1eWK0uL/8fEYEz2GCbjY -dqfJHHFSlDBD4PF4dP1hG0wJzOZoKnGtHN9DvFbbpaS+NXCkXs9P/ABVmTo9I89n -WvUi+LUp0EQR6zUuRr79jhiyX6i/GTKh9dwD5nyaHwx8qbAOITc78bA= +MIIEowIBAAKCAQEAyiyM/Peu+8FL6cYaOFLnB/jsm2Nth54ytL0iy5yup2Rl00LF +aklYe75cqNcKPZOoJ/2q6xZGjmEIykXrC+dUvNkgaWszxuZz+UDMdjQ7esZfJilW +qIJNLXgtUwfwTvUBtkhU5IivLXU5vbZxYCtSO0TV3I13VguesnqjyN1IUGSTXc7I +0AjkSmv4KwJrxK8RRMKbo0ArK6LzdAUIfA10kExeM/cbMh8IA6qck2eLhhxU8Yrp +p/dnfW5LjiMBfQ66U1QAzjs0HVz0EL2PwD+FHBhogO54iy9QR/aKjZ0kUO/F/PH3 +Hr4ozpiPYQZcwemU9AzyEHGeI2xbin3MA7q5LwIDAQABAoIBAQDFmvXS62QkvbGt +NOu70Yvuxua8mlocDAwTjCnOSb6L7h14d/LtB/NsP4vhmw1vUjsxm0bLsGIWF9G3 +os8yO1EfpDmB0D4zUlxYa3Vss3DPd8TYT99bpMA6iRQD6+Z9xgt+VwRiuxY9oC5n +t0LpdG5Tb9x4Te0uNP1QBX7AfUiJCYd4ePpbe7WBgANDXdUGbo8V6sEVie939d43 +Cijk4Q1r3bg2uOfK/nEvCH2aFfgNTO+1+0t8gXFvqM+6DobACZXEPQfV04Gj1azI +VOZ3fq9l7rawJja+LWcrYGTgUN7NamcnZIj1tCNWGmjcXoIlMcxUyRwsjUm6AP1i +5ZmtU6sBAoGBAPBuKG/NERIaYEvJgsIAVvx81Ujlg2lVxPyqxltydCZzIRO8kpGy +AaavRovw0MmLMgD9V7PF2mipjV6BcQac0rebkyV9yYLV/yKIFeVaP7QB2BPjEnbp +SgQIt9fm11+pGMTDtqoCYCFAAN6P7xaX3Qz/OLcMwRhXzG/kmBArGShfAoGBANdE +LpnloB4qE6W3mE/gZrPKu7Tk5Bg/i5RTG1CslZ6BAVPI0izUdsoMUbn7Co1G9PRg +u6lFZg9J8Tvx8Q68fQS9oAFRS0OmOu8+aL/dtsPhMFN4XlxZ+Vra6x85wcZz8cdx +ZEm2e+tRFl40t+AgcQuG4zCJdGPgPFyvglCBImExAoGAYi35oT3yPJw8unX9SU9u +Ngib5/qhIQB/QlZSTcF9IL5ewXp9t7Ui63gjrL2X5NVMhA7wI18mAxtJuU/OYc7k +VUnYWrT09tKALw+3MUMbRFyEagqN3bUCHoeY2zdOt6eLj74D94Sk0K8cK8ZG8cjt +4YLPHCC/MTuZJhAI8IFI8q0CgYAft3QGSMbmqtxqNjrCyhVXuC8f3/mbeQFfwT7t +DACfcfJ4HcaZxFQcQORpuos7dZDx0K7Vqdv3tLVOV79kpHDnGelRSGEGfp+AUHmu +i0Q6aBtusPV2Net/b3HlD+V1D/A3qoVUNwbbDP92sd3FsAH36M/gfuAfNxKttU5F +/kDKgQKBgD4WHVDW9Ey0sZoW3srKYeDyN/C0AV8eXonFvdG9hqLqs67zk6/gg3YK +U0eCHiUGiI4JKzVPKtVLfEY65/Ig8HJ96O3FzZ37xVXqForRBY2rPZgreNLhDECL +K4hiAG1YPSuvbBYbMQCwyLbDPvobbFb3MhDc1TAkFDOcnc4PTEDd -----END RSA PRIVATE KEY----- From bf581a1a5e0dce07f9de70d1afd1bccf10d0e459 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 10:48:06 -0700 Subject: [PATCH 28/31] Enforce example TLS peer verification only when a CA is configured --- examples/mqttexample.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/mqttexample.c b/examples/mqttexample.c index b5c7c0af5..9abdd1c51 100644 --- a/examples/mqttexample.c +++ b/examples/mqttexample.c @@ -640,8 +640,14 @@ static int mqtt_tls_verify_cb(int preverify, WOLFSSL_X509_STORE_CTX* store) } return 1; #else - /* Propagate wolfSSL's chain-validation result so a bad certificate + /* With no CA configured there is no trust anchor to validate against + * (getting-started/demo mode), so accept and warn. When a CA is provided + * the chain-validation result is enforced so a bad certificate * (self-signed, expired, wrong host, untrusted CA) fails the handshake. */ + if (mqttCtx != NULL && mqttCtx->ca_file == NULL) { + PRINTF(" Warning: no CA configured, skipping server authentication"); + return 1; + } return preverify; #endif } From 94475051c301dab81c85bf711b77d77870df53af Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 10:48:06 -0700 Subject: [PATCH 29/31] Harden publish encode clamp, firmware length prints, and broker partial-write handling --- examples/firmware/fwclient.c | 19 +++++++++---------- src/mqtt_broker.c | 17 +++++++++++------ src/mqtt_packet.c | 2 +- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/examples/firmware/fwclient.c b/examples/firmware/fwclient.c index 3c70c75c0..8891d24e3 100644 --- a/examples/firmware/fwclient.c +++ b/examples/firmware/fwclient.c @@ -120,29 +120,28 @@ static int fw_message_process(MQTTCtx *mqttCtx, byte* buffer, word32 len) #endif word32 remaining; - /* Validate the field sizes sequentially against the received length. - * A summed length check (sizeof(header) + sigLen + pubKeyLen + fwLen) - * overflows word32 for attacker-chosen fwLen, letting a too-short buffer - * pass and leaving pubKeyBuf/fwBuf pointing past the allocation - * (CWE-190 -> heap OOB read/write). */ + /* Validate sequentially; a summed length check overflows word32 for + * attacker-chosen fwLen (CWE-190 -> heap OOB on pubKeyBuf/fwBuf). */ if (len < sizeof(FirmwareHeader)) { - PRINTF("Message smaller than firmware header! %d", len); + PRINTF("Message smaller than firmware header! %u", (unsigned int)len); return EXIT_FAILURE; } remaining = len - sizeof(FirmwareHeader); if (header->sigLen > remaining) { - PRINTF("Firmware sigLen exceeds message! %d", header->sigLen); + PRINTF("Firmware sigLen exceeds message! %u", + (unsigned int)header->sigLen); return EXIT_FAILURE; } remaining -= header->sigLen; if (header->pubKeyLen > remaining) { - PRINTF("Firmware pubKeyLen exceeds message! %d", header->pubKeyLen); + PRINTF("Firmware pubKeyLen exceeds message! %u", + (unsigned int)header->pubKeyLen); return EXIT_FAILURE; } remaining -= header->pubKeyLen; if (header->fwLen != remaining) { - PRINTF("Message header vs. actual size mismatch! %d != %d", - header->fwLen, remaining); + PRINTF("Message header vs. actual size mismatch! %u != %u", + (unsigned int)header->fwLen, (unsigned int)remaining); return EXIT_FAILURE; } diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index 4e77b975b..ac661c5e4 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -5303,12 +5303,17 @@ static int BrokerHandle_Publish(BrokerClient* bc, int rx_len, (unsigned)pub.total_len); wr = MqttPacket_Write(&sub->client->client, sub->client->tx_buf, sub_rc); - /* On a partial/non-blocking write the subscriber's - * write.pos is left mid-packet; reset it so the next - * fan-out does not resume a stale offset and desync - * this subscriber's stream. */ - if (wr != sub_rc) { - sub->client->client.write.pos = 0; + /* Static fan-out has no per-subscriber resume queue, so + * a partial write leaves this subscriber's stream + * desynced and unrecoverable. Tear down its socket; the + * main loop reaps it on the next read error. Removal is + * deferred so next_sub stays valid this iteration. */ + if (wr != sub_rc && + sub->client->sock != BROKER_SOCKET_INVALID) { + broker->net.close(broker->net.ctx, + sub->client->sock); + sub->client->sock = BROKER_SOCKET_INVALID; + sub->client->connected = 0; } } else { diff --git a/src/mqtt_packet.c b/src/mqtt_packet.c index 7506583a5..cfe93306b 100644 --- a/src/mqtt_packet.c +++ b/src/mqtt_packet.c @@ -1952,7 +1952,7 @@ int MqttEncode_Publish(byte *tx_buf, int tx_buf_len, MqttPublish *publish, * decoded buffer_len; clamping here prevents an OOB read past the * source buffer during fan-out. */ if (publish->buffer_len > 0 && - payload_len > (int)publish->buffer_len) { + publish->buffer_len < (word32)payload_len) { payload_len = (int)publish->buffer_len; } if (tx_payload != NULL) { From 3004ba98388b13f7f20e664557d97f47aaeb6635 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 11:06:45 -0700 Subject: [PATCH 30/31] F-4927 - Enforce curl TLS hostname verification instead of bypassing for localhost --- examples/mqttnet.c | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/examples/mqttnet.c b/examples/mqttnet.c index 556497bba..19450be56 100644 --- a/examples/mqttnet.c +++ b/examples/mqttnet.c @@ -856,14 +856,13 @@ mqttcurl_connect(SocketContext* sock, const char* host, word16 port, return MQTT_CODE_ERROR_CURL; } - /* Only do server host verification when not running against - * localhost broker. */ - if (XSTRCMP(host, "localhost") == 0) { - res = curl_easy_setopt(sock->curl, CURLOPT_SSL_VERIFYHOST, 0L); - } - else { - res = curl_easy_setopt(sock->curl, CURLOPT_SSL_VERIFYHOST, 2L); - } + /* Enforce server hostname verification. The cert's CN/SAN must match + * the connect host (broker_test certs carry a localhost SAN). */ +#ifdef WOLFMQTT_ALLOW_INSECURE_TLS + res = curl_easy_setopt(sock->curl, CURLOPT_SSL_VERIFYHOST, 0L); +#else + res = curl_easy_setopt(sock->curl, CURLOPT_SSL_VERIFYHOST, 2L); +#endif if (res != CURLE_OK) { PRINTF("error: curl_easy_setopt(SSL_VERIFYHOST) returned: %d", From c138692aec62826bc9f02daf9f9ba5cd463dcae9 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 11:14:31 -0700 Subject: [PATCH 31/31] Defer nested retained-delivery reap to outermost depth to avoid use-after-free --- src/mqtt_broker.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/mqtt_broker.c b/src/mqtt_broker.c index ac661c5e4..9f454d68b 100644 --- a/src/mqtt_broker.c +++ b/src/mqtt_broker.c @@ -3797,11 +3797,19 @@ static void BrokerRetained_DeliverToClient(MqttBroker* broker, rm = broker->retained; while (rm) { BrokerRetainedMsg* rm_next = rm->next; - /* Reap expired messages and any nodes a re-entrant delete deferred - * (pending_delete). The free here is safe: rm_next is already saved. */ + /* Reap deferred-delete and expired nodes. Freeing is only safe at the + * outermost delivery depth; a nested re-entrant (WS fan-out) delivery + * may hold rm in an enclosing loop's saved rm_next, so at deeper depths + * mark the node and defer to the retained_delivering==0 post-loop reap. */ if (rm->pending_delete || (rm->expiry_sec > 0 && (now - rm->store_time) >= rm->expiry_sec)) { + if (broker->retained_delivering > 1) { + rm->pending_delete = 1; + rm_prev = rm; + rm = rm_next; + continue; + } WBLOG_DBG(broker, "broker: retained expired topic=%s", BrokerLog_Sanitize(rm->topic)); if (rm_prev) {