Skip to content

Commit b98cf60

Browse files
committed
Fixes from review
1 parent 9d3e556 commit b98cf60

5 files changed

Lines changed: 204 additions & 59 deletions

File tree

ChangeLog.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,32 @@
77
per [MQTT-1.5.3-2] / [MQTT-1.5.4-2]. Closes an embedded-NUL truncation
88
attack that allowed broker-side auth bypass, ClientId collision, and
99
topic-routing confusion when the broker compared decoded strings with
10-
C-string semantics. The check is also applied to the CONNECT Password
11-
field (which the spec defines as Binary Data) because wolfMQTT compares
12-
passwords with `XSTRLEN`/`XSTRCMP`.
10+
C-string semantics.
11+
- The CONNECT Password field is decoded by a new internal helper
12+
(`MqttDecode_Password`) that applies the same NUL rejection. Per
13+
MQTT-3.1.3.5 the field is Binary Data so the spec does not require
14+
this check, but wolfMQTT compares passwords with `XSTRLEN`/`XSTRCMP`,
15+
so a binary password with an embedded NUL would be silently truncated
16+
and could enable an auth bypass. Spec-compliant clients sending binary
17+
passwords containing 0x00 will be rejected by the broker as a result.
1318

1419
* API / Behavior Changes
1520
- `MqttDecode_String` may now return `MQTT_CODE_ERROR_MALFORMED_DATA`
16-
when the decoded string contains an embedded NUL byte. Previously only
17-
`MQTT_CODE_ERROR_BAD_ARG` and `MQTT_CODE_ERROR_OUT_OF_BUFFER` were
18-
possible negative returns.
21+
when the decoded string contains an embedded NUL byte. Previously
22+
`MQTT_CODE_ERROR_OUT_OF_BUFFER` was the only possible negative return.
1923
- `MqttDecode_Publish` now propagates the underlying error from
2024
`MqttDecode_String` (e.g. `MALFORMED_DATA`) instead of always returning
2125
`MQTT_CODE_ERROR_OUT_OF_BUFFER` on topic decode failure.
22-
- `MqttDecode_Props` similarly now propagates the underlying error from
26+
- `MqttDecode_Props` propagates `MQTT_CODE_ERROR_MALFORMED_DATA` from
2327
`MqttDecode_String` for v5 STRING and STRING_PAIR property types
24-
(Reason String, Content Type, User Property, etc.) instead of always
25-
masking it as `MQTT_CODE_ERROR_PROPERTY`.
28+
(Reason String, Content Type, User Property, etc.). Other negative
29+
returns continue to be mapped to `MQTT_CODE_ERROR_PROPERTY` so client
30+
code that branches on that error code is unaffected.
2631
- New `XMEMCHR(s, c, n)` portability macro added to `mqtt_types.h`
27-
(defaults to `memchr`). Builds using `WOLFMQTT_CUSTOM_STRING` must
28-
provide their own mapping, matching the pattern used for `XSTRLEN`,
29-
`XMEMCMP`, etc.
32+
(defaults to `memchr` for standard builds). Builds using
33+
`WOLFMQTT_CUSTOM_STRING` must define `XMEMCHR` themselves, matching
34+
the pattern used for `XSTRLEN`, `XMEMCMP`, etc.; the header emits
35+
an explicit `#error` if it is missing.
3036

3137
### v2.0.0 (03/20/2026)
3238
Release 2.0.0 has been developed according to wolfSSL's development and QA

src/mqtt_broker.c

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,18 @@ static int BrokerStrCompare(const char* a, const char* b, int cmp_len)
180180
* unaffected by string contents.
181181
* Dynamic mode: frees old value, allocates new buffer, copies. The
182182
* sensitive-wipe path uses XSTRLEN(buf)+1, which is correct only when
183-
* stored values contain no embedded NUL. MqttDecode_String enforces
184-
* [MQTT-1.5.3-2] / [MQTT-1.5.4-2] (rejects U+0000), so any string that
185-
* reaches this function via the protocol path satisfies that invariant.
186-
* Do not call the dynamic-mode sensitive path with src bytes that bypass
187-
* that decoder without first capping src_len at the first NUL, or the
188-
* wipe will leave trailing bytes uncleared. */
183+
* stored values contain no embedded NUL. The decode-side guards that
184+
* feed this function enforce that invariant:
185+
* - MqttDecode_String rejects U+0000 per [MQTT-1.5.3-2] /
186+
* [MQTT-1.5.4-2] for UTF-8 fields (client_id, username, topics).
187+
* - MqttDecode_Password applies the same NUL check to the CONNECT
188+
* Password field; that field is Binary Data per MQTT-3.1.3.5 and
189+
* the spec does not require it, but wolfMQTT compares passwords
190+
* with XSTRLEN/XSTRCMP, so the broker-side defensive check here
191+
* is what keeps the "no embedded NUL" assumption intact.
192+
* Do not call the dynamic-mode sensitive path with src bytes that
193+
* bypass those decoders without first capping src_len at the first
194+
* NUL, or the wipe will leave trailing bytes uncleared. */
189195
#ifdef WOLFMQTT_STATIC_MEMORY
190196
static void BrokerStore_String(char* dst, int max_len,
191197
const char* src, word16 src_len)

src/mqtt_packet.c

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,8 @@ int MqttEncode_Int(byte* buf, word32 len)
335335

336336
/* Returns number of buffer bytes decoded */
337337
/* Returns pointer to string (which is not guaranteed to be null terminated) */
338+
/* Note: MqttDecode_Password mirrors this implementation for the CONNECT
339+
* Password binary-data field. Keep length and bounds handling in sync. */
338340
int MqttDecode_String(byte *buf, const char **pstr, word16 *pstr_len, word32 buf_len)
339341
{
340342
int len;
@@ -349,14 +351,7 @@ int MqttDecode_String(byte *buf, const char **pstr, word16 *pstr_len, word32 buf
349351
buf += len;
350352
/* [MQTT-1.5.3-2] / [MQTT-1.5.4-2]: a UTF-8 encoded string MUST NOT
351353
* include U+0000. Reject so downstream C-string handling cannot be
352-
* tricked by an embedded NUL truncating the value.
353-
*
354-
* The CONNECT Password field is technically Binary Data per spec,
355-
* not a UTF-8 string, so the prohibition does not formally apply.
356-
* The check is still enforced here because wolfMQTT compares
357-
* passwords with XSTRLEN/XSTRCMP, so a binary password with an
358-
* embedded NUL would be silently truncated and enable an auth
359-
* bypass — exactly the class of attack this validation prevents. */
354+
* tricked by an embedded NUL truncating the value. */
360355
if (str_len > 0 && XMEMCHR(buf, 0x00, str_len) != NULL) {
361356
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA);
362357
}
@@ -653,9 +648,16 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf,
653648
(const char**)&cur_prop->data_str.str,
654649
&cur_prop->data_str.len,
655650
(word32)(buf_len - (buf - pbuf)));
656-
if (tmp < 0) {
651+
if (tmp == MQTT_CODE_ERROR_MALFORMED_DATA) {
652+
/* Propagate spec-required NUL rejection so callers can
653+
* distinguish malformed UTF-8 from generic property
654+
* decode failures. Other negative codes keep their
655+
* legacy MQTT_CODE_ERROR_PROPERTY mapping below. */
657656
rc = tmp;
658657
}
658+
else if (tmp < 0) {
659+
rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY);
660+
}
659661
else if ((word32)tmp <= (buf_len - (buf - pbuf))) {
660662
buf += tmp;
661663
total += tmp;
@@ -718,9 +720,14 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf,
718720
(const char**)&cur_prop->data_str.str,
719721
&cur_prop->data_str.len,
720722
(word32)(buf_len - (buf - pbuf)));
721-
if (tmp < 0) {
723+
/* See MQTT_DATA_TYPE_STRING above for the rationale on
724+
* propagating MALFORMED_DATA distinctly from other errors. */
725+
if (tmp == MQTT_CODE_ERROR_MALFORMED_DATA) {
722726
rc = tmp;
723727
}
728+
else if (tmp < 0) {
729+
rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY);
730+
}
724731
else if ((word32)tmp <= (buf_len - (buf - pbuf))) {
725732
buf += tmp;
726733
total += tmp;
@@ -730,9 +737,13 @@ int MqttDecode_Props(MqttPacketType packet, MqttProp** props, byte* pbuf,
730737
(const char**)&cur_prop->data_str2.str,
731738
&cur_prop->data_str2.len,
732739
(word32)(buf_len - (buf - pbuf)));
733-
if (tmp < 0) {
740+
/* See MQTT_DATA_TYPE_STRING above. */
741+
if (tmp == MQTT_CODE_ERROR_MALFORMED_DATA) {
734742
rc = tmp;
735743
}
744+
else if (tmp < 0) {
745+
rc = MQTT_TRACE_ERROR(MQTT_CODE_ERROR_PROPERTY);
746+
}
736747
else if ((word32)tmp <=
737748
(buf_len - (buf - pbuf))) {
738749
buf += tmp;
@@ -991,6 +1002,39 @@ int MqttEncode_Connect(byte *tx_buf, int tx_buf_len, MqttConnect *mc_connect)
9911002
}
9921003

9931004
#ifdef WOLFMQTT_BROKER
1005+
/* Decode the CONNECT Password field. Password is Binary Data per
1006+
* MQTT-3.1.3.5, not a UTF-8 string, so the [MQTT-1.5.3-2] / [MQTT-1.5.4-2]
1007+
* U+0000 prohibition does not formally apply. The defensive NUL check is
1008+
* applied here because wolfMQTT compares stored passwords with
1009+
* XSTRLEN/XSTRCMP, so a binary password with an embedded NUL would be
1010+
* silently truncated and could enable an auth bypass. Decoupling this
1011+
* from MqttDecode_String keeps the spec-compliant UTF-8 path separate
1012+
* from the broker-specific binary-data validation. */
1013+
static int MqttDecode_Password(byte *buf, const char **ppassword,
1014+
word16 *ppassword_len, word32 buf_len)
1015+
{
1016+
int len;
1017+
word16 pass_len;
1018+
len = MqttDecode_Num(buf, &pass_len, buf_len);
1019+
if (len < 0) {
1020+
return len;
1021+
}
1022+
if ((word32)pass_len > buf_len - (word32)len) {
1023+
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_OUT_OF_BUFFER);
1024+
}
1025+
buf += len;
1026+
if (pass_len > 0 && XMEMCHR(buf, 0x00, pass_len) != NULL) {
1027+
return MQTT_TRACE_ERROR(MQTT_CODE_ERROR_MALFORMED_DATA);
1028+
}
1029+
if (ppassword_len) {
1030+
*ppassword_len = pass_len;
1031+
}
1032+
if (ppassword) {
1033+
*ppassword = (char*)buf;
1034+
}
1035+
return len + pass_len;
1036+
}
1037+
9941038
int MqttDecode_Connect(byte *rx_buf, int rx_buf_len, MqttConnect *mc_connect)
9951039
{
9961040
int header_len, remain_len;
@@ -1172,7 +1216,7 @@ int MqttDecode_Connect(byte *rx_buf, int rx_buf_len, MqttConnect *mc_connect)
11721216
}
11731217

11741218
if (packet.flags & MQTT_CONNECT_FLAG_PASSWORD) {
1175-
tmp = MqttDecode_String(rx_payload, &mc_connect->password, NULL,
1219+
tmp = MqttDecode_Password(rx_payload, &mc_connect->password, NULL,
11761220
(word32)(rx_buf_len - (rx_payload - rx_buf)));
11771221
if (tmp < 0) {
11781222
return tmp;

tests/test_mqtt_packet.c

Lines changed: 105 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,77 @@ TEST(decode_publish_v5_content_type_property)
616616

617617
MqttProps_Free(pub.props);
618618
}
619+
620+
/* [MQTT-1.5.4-2]: an embedded NUL in a v5 STRING property must be
621+
* rejected. Uses a PUBLISH packet with a Content Type property whose
622+
* value contains 0x00. MqttDecode_Props propagates MALFORMED_DATA from
623+
* MqttDecode_String distinctly from generic MQTT_CODE_ERROR_PROPERTY. */
624+
TEST(decode_publish_v5_rejects_nul_in_string_property)
625+
{
626+
/* Wire: PUBLISH QoS 0, remain_len=13, topic "a/b", props_len=7,
627+
* prop 0x03 CONTENT_TYPE, str_len=4, "t\0xt". No payload. */
628+
byte buf[] = {
629+
0x30, 13,
630+
0x00, 0x03, 'a', '/', 'b',
631+
0x07,
632+
0x03, 0x00, 0x04, 't', 0x00, 'x', 't'
633+
};
634+
MqttPublish pub;
635+
int rc;
636+
637+
XMEMSET(&pub, 0, sizeof(pub));
638+
pub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5;
639+
rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub);
640+
ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc);
641+
MqttProps_Free(pub.props);
642+
}
643+
644+
/* STRING_PAIR (USER_PROPERTY) NUL rejection — first string of pair. The
645+
* MqttDecode_Props path runs MqttDecode_String twice for STRING_PAIR;
646+
* this pins coverage on the first sub-decode propagating MALFORMED_DATA. */
647+
TEST(decode_publish_v5_rejects_nul_in_user_prop_key)
648+
{
649+
/* Wire: PUBLISH QoS 0, remain_len=14, topic "a/b", props_len=8,
650+
* prop 0x26 USER_PROPERTY, key_len=2 "k\0", val_len=1 "v". */
651+
byte buf[] = {
652+
0x30, 14,
653+
0x00, 0x03, 'a', '/', 'b',
654+
0x08,
655+
0x26, 0x00, 0x02, 'k', 0x00,
656+
0x00, 0x01, 'v'
657+
};
658+
MqttPublish pub;
659+
int rc;
660+
661+
XMEMSET(&pub, 0, sizeof(pub));
662+
pub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5;
663+
rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub);
664+
ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc);
665+
MqttProps_Free(pub.props);
666+
}
667+
668+
/* STRING_PAIR (USER_PROPERTY) NUL rejection — second string of pair.
669+
* Pins coverage on the second sub-decode propagating MALFORMED_DATA. */
670+
TEST(decode_publish_v5_rejects_nul_in_user_prop_value)
671+
{
672+
/* Wire: PUBLISH QoS 0, remain_len=14, topic "a/b", props_len=8,
673+
* prop 0x26 USER_PROPERTY, key_len=1 "k", val_len=2 "v\0". */
674+
byte buf[] = {
675+
0x30, 14,
676+
0x00, 0x03, 'a', '/', 'b',
677+
0x08,
678+
0x26, 0x00, 0x01, 'k',
679+
0x00, 0x02, 'v', 0x00
680+
};
681+
MqttPublish pub;
682+
int rc;
683+
684+
XMEMSET(&pub, 0, sizeof(pub));
685+
pub.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5;
686+
rc = MqttDecode_Publish(buf, (int)sizeof(buf), &pub);
687+
ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc);
688+
MqttProps_Free(pub.props);
689+
}
619690
#endif /* WOLFMQTT_V5 */
620691

621692
/* ============================================================================
@@ -1610,6 +1681,34 @@ TEST(decode_connect_rejects_nul_in_will_topic)
16101681
rc = MqttDecode_Connect(buf, (int)sizeof(buf), &dec);
16111682
ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc);
16121683
}
1684+
1685+
#ifdef WOLFMQTT_V5
1686+
/* MQTT v5 has a different decode path than v3.1.1 (properties walked
1687+
* before client_id, separate LWT properties), but the NUL rejection
1688+
* lives inside MqttDecode_String / MqttDecode_Password and so applies
1689+
* uniformly. This pins coverage on the v5 branch so a future refactor
1690+
* cannot quietly bypass the check on one protocol level. */
1691+
TEST(decode_connect_v5_rejects_nul_in_client_id)
1692+
{
1693+
byte buf[] = {
1694+
0x10, 0x13, /* CONNECT, remain_len = 19 */
1695+
0x00, 0x04, 'M', 'Q', 'T', 'T',
1696+
0x05, /* protocol level v5 */
1697+
0x02, /* flags: clean_session */
1698+
0x00, 0x3C, /* keep alive */
1699+
0x00, /* props_len = 0 (VBI) */
1700+
0x00, 0x06, 'a', 'd', 0x00, 'm', 'i', 'n' /* client_id with NUL */
1701+
};
1702+
MqttConnect dec;
1703+
int rc;
1704+
1705+
XMEMSET(&dec, 0, sizeof(dec));
1706+
dec.protocol_level = MQTT_CONNECT_PROTOCOL_LEVEL_5;
1707+
rc = MqttDecode_Connect(buf, (int)sizeof(buf), &dec);
1708+
ASSERT_EQ(MQTT_CODE_ERROR_MALFORMED_DATA, rc);
1709+
MqttProps_Free(dec.props);
1710+
}
1711+
#endif /* WOLFMQTT_V5 */
16131712
#endif /* WOLFMQTT_BROKER */
16141713

16151714
/* ============================================================================
@@ -1726,31 +1825,6 @@ TEST(decode_subscribe_v5_options_byte_qos_extracted)
17261825
ASSERT_EQ(1, sub.topic_count);
17271826
ASSERT_EQ(MQTT_QOS_1, topic_arr[0].qos);
17281827
}
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-
}
17541828
#endif /* WOLFMQTT_V5 */
17551829

17561830
/* ============================================================================
@@ -2311,6 +2385,9 @@ void run_mqtt_packet_tests(void)
23112385
RUN_TEST(decode_publish_rejects_nul_in_topic);
23122386
#ifdef WOLFMQTT_V5
23132387
RUN_TEST(decode_publish_v5_content_type_property);
2388+
RUN_TEST(decode_publish_v5_rejects_nul_in_string_property);
2389+
RUN_TEST(decode_publish_v5_rejects_nul_in_user_prop_key);
2390+
RUN_TEST(decode_publish_v5_rejects_nul_in_user_prop_value);
23142391
#endif
23152392

23162393
/* MqttDecode_ConnectAck */
@@ -2364,14 +2441,16 @@ void run_mqtt_packet_tests(void)
23642441
RUN_TEST(decode_connect_rejects_nul_in_username);
23652442
RUN_TEST(decode_connect_rejects_nul_in_password);
23662443
RUN_TEST(decode_connect_rejects_nul_in_will_topic);
2444+
#ifdef WOLFMQTT_V5
2445+
RUN_TEST(decode_connect_v5_rejects_nul_in_client_id);
2446+
#endif
23672447

23682448
/* MqttDecode_Subscribe */
23692449
RUN_TEST(decode_subscribe_v311_single_topic);
23702450
RUN_TEST(decode_subscribe_v311_qos3_reserved);
23712451
RUN_TEST(decode_subscribe_rejects_nul_in_filter);
23722452
#ifdef WOLFMQTT_V5
23732453
RUN_TEST(decode_subscribe_v5_options_byte_qos_extracted);
2374-
RUN_TEST(decode_publish_v5_rejects_nul_in_string_property);
23752454
#endif
23762455

23772456
/* MqttDecode_Unsubscribe */

wolfmqtt/mqtt_types.h

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,6 @@ enum MqttPacketResponseCodes {
246246
#ifndef XMEMCMP
247247
#define XMEMCMP(s1,s2,n) memcmp((s1),(s2),(n))
248248
#endif
249-
#ifndef XMEMCHR
250-
#define XMEMCHR(s,c,n) memchr((s),(c),(n))
251-
#endif
252249
#ifndef XATOI
253250
#define XATOI(s) atoi((s))
254251
#endif
@@ -267,6 +264,19 @@ enum MqttPacketResponseCodes {
267264
#endif
268265
#endif
269266

267+
/* XMEMCHR backstop. Standard builds and existing custom-string ports
268+
* (which already pull in <string.h> via other paths) keep building
269+
* without changes. Custom-string ports that intentionally avoid
270+
* <string.h> get an explicit #error directing them to define XMEMCHR
271+
* themselves, instead of a confusing missing-header diagnostic. */
272+
#ifndef XMEMCHR
273+
#ifdef WOLFMQTT_CUSTOM_STRING
274+
#error "WOLFMQTT_CUSTOM_STRING set: please define XMEMCHR"
275+
#else
276+
#define XMEMCHR(s,c,n) memchr((s),(c),(n))
277+
#endif
278+
#endif
279+
270280
#ifndef WOLFMQTT_CUSTOM_MALLOC
271281
#ifndef WOLFMQTT_MALLOC
272282
#define WOLFMQTT_MALLOC(s) malloc((s))

0 commit comments

Comments
 (0)