@@ -555,6 +555,23 @@ TEST(decode_publish_malformed_variable_exceeds_remain)
555555 ASSERT_EQ (MQTT_CODE_ERROR_OUT_OF_BUFFER , rc );
556556}
557557
558+ /* [MQTT-1.5.3-2] / [MQTT-4.7.3-2]: a topic name containing U+0000 must be
559+ * rejected. Without this check, downstream broker logic uses C-string
560+ * semantics on the stored topic and a publish to "se\0cret" would route
561+ * to subscribers of "se". */
562+ TEST (decode_publish_rejects_nul_in_topic )
563+ {
564+ byte buf [] = { 0x30 , 10 ,
565+ 0x00 , 0x07 , 's' , 'e' , 0x00 , 'c' , 'r' , 'e' , 't' ,
566+ 'X' };
567+ MqttPublish pub ;
568+ int rc ;
569+
570+ XMEMSET (& pub , 0 , sizeof (pub ));
571+ rc = MqttDecode_Publish (buf , (int )sizeof (buf ), & pub );
572+ ASSERT_EQ (MQTT_CODE_ERROR_MALFORMED_DATA , rc );
573+ }
574+
558575#ifdef WOLFMQTT_V5
559576/* Hand-validated MQTT v5 PUBLISH packet (independent oracle, not produced by
560577 * MqttEncode_Publish) so encode and decode cannot hide a shared bug:
@@ -1501,6 +1518,98 @@ TEST(decode_connect_v311_with_lwt)
15011518 ASSERT_NOT_NULL (dec_lwt .buffer );
15021519 ASSERT_EQ (0 , XMEMCMP (dec_lwt .buffer , lwt_payload , sizeof (lwt_payload )));
15031520}
1521+
1522+ /* [MQTT-1.5.3-2]: an embedded NUL in the ClientId must be rejected.
1523+ * Otherwise BrokerClient_FindByClientId() (which uses XSTRCMP) will treat
1524+ * "ad\0min" as "ad" and collide with an existing "ad" session. */
1525+ TEST (decode_connect_rejects_nul_in_client_id )
1526+ {
1527+ byte buf [] = {
1528+ 0x10 , 0x12 , /* CONNECT, remain_len = 18 */
1529+ 0x00 , 0x04 , 'M' , 'Q' , 'T' , 'T' , /* protocol name */
1530+ 0x04 , /* protocol level v3.1.1 */
1531+ 0x02 , /* flags: clean_session */
1532+ 0x00 , 0x3C , /* keep alive */
1533+ 0x00 , 0x06 , 'a' , 'd' , 0x00 , 'm' , 'i' , 'n' /* client_id with NUL */
1534+ };
1535+ MqttConnect dec ;
1536+ int rc ;
1537+
1538+ XMEMSET (& dec , 0 , sizeof (dec ));
1539+ rc = MqttDecode_Connect (buf , (int )sizeof (buf ), & dec );
1540+ ASSERT_EQ (MQTT_CODE_ERROR_MALFORMED_DATA , rc );
1541+ }
1542+
1543+ /* [MQTT-1.5.3-2]: an embedded NUL in the username must be rejected.
1544+ * Otherwise BrokerStrCompare() (which uses XSTRLEN) will treat
1545+ * "us\0er" as "us" and accept it against a configured "us" credential. */
1546+ TEST (decode_connect_rejects_nul_in_username )
1547+ {
1548+ byte buf [] = {
1549+ 0x10 , 0x15 , /* CONNECT, remain_len = 21 */
1550+ 0x00 , 0x04 , 'M' , 'Q' , 'T' , 'T' ,
1551+ 0x04 ,
1552+ 0x82 , /* clean_session + USERNAME */
1553+ 0x00 , 0x3C ,
1554+ 0x00 , 0x02 , 'c' , '1' , /* client_id "c1" */
1555+ 0x00 , 0x05 , 'u' , 's' , 0x00 , 'e' , 'r' /* username with NUL */
1556+ };
1557+ MqttConnect dec ;
1558+ int rc ;
1559+
1560+ XMEMSET (& dec , 0 , sizeof (dec ));
1561+ rc = MqttDecode_Connect (buf , (int )sizeof (buf ), & dec );
1562+ ASSERT_EQ (MQTT_CODE_ERROR_MALFORMED_DATA , rc );
1563+ }
1564+
1565+ /* [MQTT-1.5.3-2]: an embedded NUL in the password must be rejected.
1566+ * Same auth-bypass mechanism as the username test, applied to the
1567+ * password field. */
1568+ TEST (decode_connect_rejects_nul_in_password )
1569+ {
1570+ byte buf [] = {
1571+ 0x10 , 0x16 , /* CONNECT, remain_len = 22 */
1572+ 0x00 , 0x04 , 'M' , 'Q' , 'T' , 'T' ,
1573+ 0x04 ,
1574+ 0xC2 , /* clean_session + USER + PASS */
1575+ 0x00 , 0x3C ,
1576+ 0x00 , 0x02 , 'c' , '1' ,
1577+ 0x00 , 0x01 , 'u' , /* username "u" */
1578+ 0x00 , 0x03 , 'p' , 0x00 , 'w' /* password with NUL */
1579+ };
1580+ MqttConnect dec ;
1581+ int rc ;
1582+
1583+ XMEMSET (& dec , 0 , sizeof (dec ));
1584+ rc = MqttDecode_Connect (buf , (int )sizeof (buf ), & dec );
1585+ ASSERT_EQ (MQTT_CODE_ERROR_MALFORMED_DATA , rc );
1586+ }
1587+
1588+ /* [MQTT-1.5.3-2] / [MQTT-4.7.3-2]: a Will Topic with embedded NUL must
1589+ * be rejected. The same C-string truncation that affects PUBLISH topics
1590+ * applies to Will Topics persisted by the broker. */
1591+ TEST (decode_connect_rejects_nul_in_will_topic )
1592+ {
1593+ byte buf [] = {
1594+ 0x10 , 0x16 , /* CONNECT, remain_len = 22 */
1595+ 0x00 , 0x04 , 'M' , 'Q' , 'T' , 'T' ,
1596+ 0x04 ,
1597+ 0x06 , /* clean_session + WILL_FLAG */
1598+ 0x00 , 0x3C ,
1599+ 0x00 , 0x02 , 'c' , '1' , /* client_id */
1600+ 0x00 , 0x03 , 't' , 0x00 , 'p' , /* will topic with NUL */
1601+ 0x00 , 0x01 , 'X' /* will payload */
1602+ };
1603+ MqttConnect dec ;
1604+ MqttMessage lwt ;
1605+ int rc ;
1606+
1607+ XMEMSET (& dec , 0 , sizeof (dec ));
1608+ XMEMSET (& lwt , 0 , sizeof (lwt ));
1609+ dec .lwt_msg = & lwt ;
1610+ rc = MqttDecode_Connect (buf , (int )sizeof (buf ), & dec );
1611+ ASSERT_EQ (MQTT_CODE_ERROR_MALFORMED_DATA , rc );
1612+ }
15041613#endif /* WOLFMQTT_BROKER */
15051614
15061615/* ============================================================================
@@ -1564,6 +1673,28 @@ TEST(decode_subscribe_v311_qos3_reserved)
15641673 ASSERT_EQ (MQTT_QOS_3 , topic_arr [0 ].qos );
15651674}
15661675
1676+ /* [MQTT-1.5.3-2] / [MQTT-4.7.3-2]: a topic filter containing U+0000 must
1677+ * be rejected. Without this check, a stored filter "a\0b" would match
1678+ * topic "a" once iteration hits the embedded NUL in BrokerTopicMatch. */
1679+ TEST (decode_subscribe_rejects_nul_in_filter )
1680+ {
1681+ byte rx_buf [] = {
1682+ 0x82 , 0x08 ,
1683+ 0x00 , 0x01 , /* packet_id */
1684+ 0x00 , 0x03 , 'a' , 0x00 , 'b' , /* filter "a\0b" */
1685+ 0x00 /* options: QoS 0 */
1686+ };
1687+ MqttSubscribe sub ;
1688+ MqttTopic topic_arr [1 ];
1689+ int rc ;
1690+
1691+ XMEMSET (& sub , 0 , sizeof (sub ));
1692+ XMEMSET (topic_arr , 0 , sizeof (topic_arr ));
1693+ sub .topics = topic_arr ;
1694+ rc = MqttDecode_Subscribe (rx_buf , (int )sizeof (rx_buf ), & sub );
1695+ ASSERT_EQ (MQTT_CODE_ERROR_MALFORMED_DATA , rc );
1696+ }
1697+
15671698#ifdef WOLFMQTT_V5
15681699/* [MQTT-3.8.3] v5 SUBSCRIBE options byte carries QoS (bits 0-1), No Local
15691700 * (bit 2), Retain As Published (bit 3), and Retain Handling (bits 4-5).
@@ -1595,7 +1726,57 @@ TEST(decode_subscribe_v5_options_byte_qos_extracted)
15951726 ASSERT_EQ (1 , sub .topic_count );
15961727 ASSERT_EQ (MQTT_QOS_1 , topic_arr [0 ].qos );
15971728}
1729+
1730+ /* [MQTT-1.5.4-2]: an embedded NUL in a v5 STRING property must be
1731+ * rejected. Uses a PUBLISH packet with a Content Type property whose
1732+ * value contains 0x00. The MqttDecode_Props path now propagates the
1733+ * underlying MALFORMED_DATA from MqttDecode_String instead of masking
1734+ * it as MQTT_CODE_ERROR_PROPERTY. */
1735+ TEST (decode_publish_v5_rejects_nul_in_string_property )
1736+ {
1737+ /* Wire: PUBLISH QoS 0, remain_len=13, topic "a/b", props_len=7,
1738+ * prop 0x03 CONTENT_TYPE, str_len=4, "t\0xt". No payload. */
1739+ byte buf [] = {
1740+ 0x30 , 13 ,
1741+ 0x00 , 0x03 , 'a' , '/' , 'b' ,
1742+ 0x07 ,
1743+ 0x03 , 0x00 , 0x04 , 't' , 0x00 , 'x' , 't'
1744+ };
1745+ MqttPublish pub ;
1746+ int rc ;
1747+
1748+ XMEMSET (& pub , 0 , sizeof (pub ));
1749+ pub .protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5 ;
1750+ rc = MqttDecode_Publish (buf , (int )sizeof (buf ), & pub );
1751+ ASSERT_EQ (MQTT_CODE_ERROR_MALFORMED_DATA , rc );
1752+ MqttProps_Free (pub .props );
1753+ }
15981754#endif /* WOLFMQTT_V5 */
1755+
1756+ /* ============================================================================
1757+ * MqttDecode_Unsubscribe (broker-side)
1758+ * ============================================================================ */
1759+
1760+ /* [MQTT-1.5.3-2] / [MQTT-4.7.3-2]: a topic filter containing U+0000 in an
1761+ * UNSUBSCRIBE must be rejected — MqttDecode_Unsubscribe shares the same
1762+ * MqttDecode_String chokepoint that SUBSCRIBE uses. */
1763+ TEST (decode_unsubscribe_rejects_nul_in_filter )
1764+ {
1765+ byte rx_buf [] = {
1766+ 0xA2 , 0x07 ,
1767+ 0x00 , 0x01 , /* packet_id */
1768+ 0x00 , 0x03 , 'a' , 0x00 , 'b' /* filter "a\0b" */
1769+ };
1770+ MqttUnsubscribe unsub ;
1771+ MqttTopic topic_arr [1 ];
1772+ int rc ;
1773+
1774+ XMEMSET (& unsub , 0 , sizeof (unsub ));
1775+ XMEMSET (topic_arr , 0 , sizeof (topic_arr ));
1776+ unsub .topics = topic_arr ;
1777+ rc = MqttDecode_Unsubscribe (rx_buf , (int )sizeof (rx_buf ), & unsub );
1778+ ASSERT_EQ (MQTT_CODE_ERROR_MALFORMED_DATA , rc );
1779+ }
15991780#endif /* WOLFMQTT_BROKER */
16001781
16011782/* ============================================================================
@@ -2127,6 +2308,7 @@ void run_mqtt_packet_tests(void)
21272308 RUN_TEST (decode_publish_qos1_valid );
21282309 RUN_TEST (decode_publish_qos0_zero_payload );
21292310 RUN_TEST (decode_publish_malformed_variable_exceeds_remain );
2311+ RUN_TEST (decode_publish_rejects_nul_in_topic );
21302312#ifdef WOLFMQTT_V5
21312313 RUN_TEST (decode_publish_v5_content_type_property );
21322314#endif
@@ -2178,13 +2360,22 @@ void run_mqtt_packet_tests(void)
21782360 RUN_TEST (decode_connect_wrong_protocol_name );
21792361 RUN_TEST (decode_connect_wrong_protocol_length );
21802362 RUN_TEST (decode_connect_v311_with_lwt );
2363+ RUN_TEST (decode_connect_rejects_nul_in_client_id );
2364+ RUN_TEST (decode_connect_rejects_nul_in_username );
2365+ RUN_TEST (decode_connect_rejects_nul_in_password );
2366+ RUN_TEST (decode_connect_rejects_nul_in_will_topic );
21812367
21822368 /* MqttDecode_Subscribe */
21832369 RUN_TEST (decode_subscribe_v311_single_topic );
21842370 RUN_TEST (decode_subscribe_v311_qos3_reserved );
2371+ RUN_TEST (decode_subscribe_rejects_nul_in_filter );
21852372#ifdef WOLFMQTT_V5
21862373 RUN_TEST (decode_subscribe_v5_options_byte_qos_extracted );
2374+ RUN_TEST (decode_publish_v5_rejects_nul_in_string_property );
21872375#endif
2376+
2377+ /* MqttDecode_Unsubscribe */
2378+ RUN_TEST (decode_unsubscribe_rejects_nul_in_filter );
21882379#endif
21892380
21902381 /* QoS 2 ack arithmetic */
0 commit comments