@@ -2696,6 +2696,8 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len,
26962696 MqttConnect mc ;
26972697 MqttConnectAck ack ;
26982698 MqttMessage lwt ;
2699+ word16 id_len = 0 ;
2700+ int auto_assigned = 0 ;
26992701
27002702 XMEMSET (& mc , 0 , sizeof (mc ));
27012703 XMEMSET (& ack , 0 , sizeof (ack ));
@@ -2725,7 +2727,6 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len,
27252727 bc -> client_id [0 ] = '\0' ;
27262728#endif
27272729 if (mc .client_id ) {
2728- word16 id_len = 0 ;
27292730 if (MqttDecode_Num ((byte * )mc .client_id - MQTT_DATA_LEN_SIZE ,
27302731 & id_len , MQTT_DATA_LEN_SIZE ) == MQTT_DATA_LEN_SIZE ) {
27312732 #ifdef WOLFMQTT_STATIC_MEMORY
@@ -2747,14 +2748,101 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len,
27472748 goto send_connack ;
27482749 }
27492750 #endif
2750- BROKER_STORE_STR (bc -> client_id , mc .client_id , id_len ,
2751- BROKER_MAX_CLIENT_ID_LEN );
2751+ if (id_len > 0 ) {
2752+ BROKER_STORE_STR (bc -> client_id , mc .client_id , id_len ,
2753+ BROKER_MAX_CLIENT_ID_LEN );
2754+ }
2755+ }
2756+ }
2757+
2758+ /* Reserve the "auto-" prefix for server-assigned IDs. Without this an
2759+ * attacker could observe their own assigned auto-XXXXXXXX, then reconnect
2760+ * with an explicit client_id matching a predicted future value and
2761+ * hijack a victim's session via the duplicate-takeover path below. */
2762+ if (id_len >= 5 && BROKER_STR_VALID (bc -> client_id ) &&
2763+ XSTRNCMP (bc -> client_id , "auto-" , 5 ) == 0 ) {
2764+ WBLOG_ERR (broker ,
2765+ "broker: client_id with reserved 'auto-' prefix sock=%d" ,
2766+ (int )bc -> sock );
2767+ #ifdef WOLFMQTT_V5
2768+ if (mc .protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 ) {
2769+ ack .return_code = MQTT_REASON_CLIENT_ID_NOT_VALID ;
2770+ }
2771+ else
2772+ #endif
2773+ {
2774+ ack .return_code = MQTT_CONNECT_ACK_CODE_REFUSED_ID ;
27522775 }
2776+ goto send_connack ;
2777+ }
2778+
2779+ /* [MQTT-3.1.3-8] v3.1.1: zero-length ClientId requires CleanSession=1.
2780+ * The server MUST respond with CONNACK 0x02 (Identifier rejected) and
2781+ * close the connection. v5 dropped this restriction in favor of Clean
2782+ * Start + Session Expiry Interval, so it is enforced for v3.1.1 only. */
2783+ if (id_len == 0 && !mc .clean_session
2784+ #ifdef WOLFMQTT_V5
2785+ && mc .protocol_level < MQTT_CONNECT_PROTOCOL_LEVEL_5
2786+ #endif
2787+ ) {
2788+ WBLOG_ERR (broker ,
2789+ "broker: empty ClientId with clean_session=0 sock=%d "
2790+ "[MQTT-3.1.3-8]" , (int )bc -> sock );
2791+ ack .return_code = MQTT_CONNECT_ACK_CODE_REFUSED_ID ;
2792+ goto send_connack ;
27532793 }
27542794
27552795 bc -> protocol_level = mc .protocol_level ;
27562796 bc -> keep_alive_sec = mc .keep_alive_sec ;
27572797 bc -> last_rx = WOLFMQTT_BROKER_GET_TIME_S ();
2798+
2799+ /* [MQTT-3.1.3-6] If we accepted a zero-length ClientId, assign a unique
2800+ * server-generated one before the duplicate-check / session-resume block
2801+ * below so the assigned ID flows through normal handling. v5 also echoes
2802+ * it back to the client via the Assigned Client Identifier property
2803+ * (emitted in the v5 CONNACK construction below); v3.1.1 has no such
2804+ * field, so the assignment is server-internal. */
2805+ if (id_len == 0 && !BROKER_STR_VALID (bc -> client_id )) {
2806+ /* "auto-" + at least 8 hex chars + NUL. On a 16-bit-int platform the
2807+ * full 32-bit counter prints as more than 8 nibbles, so size for
2808+ * headroom rather than the minimum. The counter advances on every
2809+ * empty-ID CONNECT that reaches this point — including connects that
2810+ * are later refused (e.g., auth failure below) — so it is not a
2811+ * count of accepted clients. */
2812+ char auto_id [32 ];
2813+ int auto_len ;
2814+ unsigned long id_value = (unsigned long )broker -> next_auto_id ++ ;
2815+ if (broker -> next_auto_id == 0 ) {
2816+ /* Skip 0 on wrap for stylistic consistency with next_packet_id;
2817+ * unlike packet IDs, 0 has no protocol significance here. */
2818+ broker -> next_auto_id = 1 ;
2819+ }
2820+ auto_len = XSNPRINTF (auto_id , (int )sizeof (auto_id ),
2821+ "auto-%08lx" , id_value );
2822+ if (auto_len > 0 ) {
2823+ BROKER_STORE_STR (bc -> client_id , auto_id , (word16 )auto_len ,
2824+ BROKER_MAX_CLIENT_ID_LEN );
2825+ }
2826+ if (!BROKER_STR_VALID (bc -> client_id )) {
2827+ /* Storage failed (e.g., WOLFMQTT_MALLOC returned NULL in the
2828+ * dynamic-memory path). Refuse rather than proceeding with an
2829+ * untracked client. */
2830+ WBLOG_ERR (broker ,
2831+ "broker: auto-id store failed sock=%d" , (int )bc -> sock );
2832+ #ifdef WOLFMQTT_V5
2833+ if (mc .protocol_level >= MQTT_CONNECT_PROTOCOL_LEVEL_5 ) {
2834+ ack .return_code = MQTT_REASON_SERVER_UNAVAILABLE ;
2835+ }
2836+ else
2837+ #endif
2838+ {
2839+ ack .return_code = MQTT_CONNECT_ACK_CODE_REFUSED_UNAVAIL ;
2840+ }
2841+ goto send_connack ;
2842+ }
2843+ auto_assigned = 1 ;
2844+ }
2845+
27582846 WBLOG_INFO (broker , "broker: CONNECT proto=%u clean=%d will=%d client_id=%s" ,
27592847 mc .protocol_level , mc .clean_session , mc .enable_lwt ,
27602848 BROKER_STR_VALID (bc -> client_id ) ? bc -> client_id : "(null)" );
@@ -2986,25 +3074,16 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len,
29863074 ack .return_code == MQTT_CONNECT_ACK_CODE_ACCEPTED ) {
29873075 MqttProp * prop ;
29883076
2989- /* If client sent empty client ID, generate one and inform client */
2990- if (!BROKER_STR_VALID (bc -> client_id )) {
2991- char auto_id [32 ];
2992- int id_len = XSNPRINTF (auto_id , (int )sizeof (auto_id ),
2993- "auto-%04x" , broker -> next_packet_id ++ );
2994- if (broker -> next_packet_id == 0 ) {
2995- broker -> next_packet_id = 1 ;
2996- }
2997- if (id_len > 0 ) {
2998- BROKER_STORE_STR (bc -> client_id , auto_id , (word16 )id_len ,
2999- BROKER_MAX_CLIENT_ID_LEN );
3000- }
3001- if (BROKER_STR_VALID (bc -> client_id )) {
3002- prop = MqttProps_Add (& ack .props );
3003- if (prop != NULL ) {
3004- prop -> type = MQTT_PROP_ASSIGNED_CLIENT_ID ;
3005- prop -> data_str .str = bc -> client_id ;
3006- prop -> data_str .len = (word16 )XSTRLEN (bc -> client_id );
3007- }
3077+ /* [MQTT-3.1.3-6] Echo any server-assigned ClientId to v5 clients.
3078+ * Keyed off auto_assigned (set in the auto-id branch above) so this
3079+ * stays true to its name even if a future code path populates
3080+ * bc->client_id from another source (e.g., a TLS-cert identity). */
3081+ if (auto_assigned ) {
3082+ prop = MqttProps_Add (& ack .props );
3083+ if (prop != NULL ) {
3084+ prop -> type = MQTT_PROP_ASSIGNED_CLIENT_ID ;
3085+ prop -> data_str .str = bc -> client_id ;
3086+ prop -> data_str .len = (word16 )XSTRLEN (bc -> client_id );
30083087 }
30093088 }
30103089
@@ -3035,9 +3114,7 @@ static int BrokerHandle_Connect(BrokerClient* bc, int rx_len,
30353114 }
30363115#endif
30373116
3038- #if defined(WOLFMQTT_BROKER_WILL ) || defined(WOLFMQTT_STATIC_MEMORY )
30393117send_connack :
3040- #endif
30413118 rc = MqttEncode_ConnectAck (bc -> tx_buf , BROKER_CLIENT_TX_SZ (bc ), & ack );
30423119 if (rc > 0 ) {
30433120 WBLOG_INFO (broker , "broker: CONNACK send sock=%d code=%d" , (int )bc -> sock ,
@@ -3729,6 +3806,7 @@ int MqttBroker_Init(MqttBroker* broker, MqttBrokerNet* net)
37293806 broker -> running = 0 ;
37303807 broker -> log_level = BROKER_LOG_LEVEL_DEFAULT ;
37313808 broker -> next_packet_id = 1 ;
3809+ broker -> next_auto_id = 1 ;
37323810
37333811#if !defined(WOLFMQTT_WOLFIP ) && !defined(WOLFMQTT_BROKER_CUSTOM_NET )
37343812 /* For the default POSIX backend, the net callbacks expect ctx to be a
0 commit comments