Commit 03485e1f4c for freeswitch.com
commit 03485e1f4c21ff04a7e4339e18a1745a3235fa1c
Author: Dmitry Verenitsin <morbit85@gmail.com>
Date: Sun Aug 9 00:33:58 2026 +0500
[core] Harden STUN attribute parsing bounds and USERNAME copy (#3112)
Bounds and termination fixes across the STUN attribute receive path
in `handle_ice` and `switch_stun_lookup`:
- `switch_stun_packet_next_attribute` and its `_hbo` variant now
confirm the 4-byte attribute header is fully within `end` before
dereferencing `type`/`length`, and include the header when checking
that the value fits, so a truncated or overrunning attribute is not
read past the buffer.
- Compute `end_buf` as the 20-byte STUN header plus the attribute
section (`SWITCH_STUN_PACKET_MIN_LEN + header.length`) so the walk
covers every attribute, including trailing ones.
- Make `switch_stun_packet_next_attribute` the sole loop terminator
and drop the redundant `xlen` guard; its seed differed between the
two functions and could skip a trailing attribute in
`switch_stun_lookup`.
- `switch_stun_packet_attribute_get_username` reserves a byte for the
terminator and always NUL-terminates, since callers use the result
as a C string.
diff --git a/src/include/switch_stun.h b/src/include/switch_stun.h
index a2d6f9b2cd..9558ae347c 100644
--- a/src/include/switch_stun.h
+++ b/src/include/switch_stun.h
@@ -313,9 +313,9 @@ SWITCH_DECLARE(switch_status_t) switch_stun_ip_lookup(char **external_ip, const
\return true or false depending on if there are any more attributes
*/
-#define switch_stun_packet_next_attribute(attribute, end) (attribute && (attribute = (switch_stun_packet_attribute_t *) (attribute->value + switch_stun_attribute_padded_length(attribute))) && ((void *)attribute < end) && attribute->type && (((switch_byte_t *)attribute + switch_stun_attribute_padded_length(attribute)) < (switch_byte_t *)end))
+#define switch_stun_packet_next_attribute(attribute, end) (attribute && (attribute = (switch_stun_packet_attribute_t *) (attribute->value + switch_stun_attribute_padded_length(attribute))) && ((switch_byte_t *)(attribute + 1) <= (switch_byte_t *)(end)) && attribute->type && (((switch_byte_t *)attribute->value + switch_stun_attribute_padded_length(attribute)) <= (switch_byte_t *)end))
-#define switch_stun_packet_next_attribute_hbo(attribute, end) (attribute && (attribute = (switch_stun_packet_attribute_t *) (attribute->value + switch_stun_attribute_padded_length_hbo(attribute))) && ((void *)attribute < end) && attribute->type && (((switch_byte_t *)attribute + switch_stun_attribute_padded_length_hbo(attribute)) < (switch_byte_t *)end))
+#define switch_stun_packet_next_attribute_hbo(attribute, end) (attribute && (attribute = (switch_stun_packet_attribute_t *) (attribute->value + switch_stun_attribute_padded_length_hbo(attribute))) && ((switch_byte_t *)(attribute + 1) <= (switch_byte_t *)(end)) && attribute->type && (((switch_byte_t *)attribute->value + switch_stun_attribute_padded_length_hbo(attribute)) <= (switch_byte_t *)end))
/*!
\brief Obtain the correct length in bytes of a stun packet
diff --git a/src/switch_rtp.c b/src/switch_rtp.c
index 15bab720fa..d0ff5eb40a 100644
--- a/src/switch_rtp.c
+++ b/src/switch_rtp.c
@@ -1013,7 +1013,6 @@ static void handle_ice(switch_rtp_t *rtp_session, switch_rtp_ice_t *ice, void *d
char username[STUN_USERNAME_MAX_SIZE] = { 0 };
unsigned char buf[1500] = { 0 };
switch_size_t cpylen = len;
- int xlen = 0;
int ok = 1;
uint32_t *pri = NULL;
int is_rtcp = ice == &rtp_session->rtcp_ice;
@@ -1100,7 +1099,7 @@ static void handle_ice(switch_rtp_t *rtp_session, switch_rtp_ice_t *ice, void *d
calc_elapsed(rtp_session, ice);
- end_buf = buf + ((sizeof(buf) > packet->header.length) ? packet->header.length : sizeof(buf));
+ end_buf = buf + ((sizeof(buf) > SWITCH_STUN_PACKET_MIN_LEN + packet->header.length) ? SWITCH_STUN_PACKET_MIN_LEN + packet->header.length : sizeof(buf));
switch_stun_packet_first_attribute(packet, attr);
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(rtp_session->session), SWITCH_LOG_DEBUG8, "%s STUN PACKET TYPE: %s\n",
@@ -1179,12 +1178,7 @@ static void handle_ice(switch_rtp_t *rtp_session, switch_rtp_ice_t *ice, void *d
break;
}
- if (!switch_stun_packet_next_attribute(attr, end_buf)) {
- break;
- }
-
- xlen += 4 + switch_stun_attribute_padded_length(attr);
- } while (xlen <= packet->header.length);
+ } while (switch_stun_packet_next_attribute(attr, end_buf));
if ((ice->type & ICE_GOOGLE_JINGLE) && ok) {
ok = !strcmp(ice->user_ice, username);
diff --git a/src/switch_stun.c b/src/switch_stun.c
index 689990e8e3..93a571f783 100644
--- a/src/switch_stun.c
+++ b/src/switch_stun.c
@@ -459,8 +459,16 @@ SWITCH_DECLARE(char *) switch_stun_packet_attribute_get_username(switch_stun_pac
{
uint16_t cpylen;
- cpylen = attribute->length < len ? attribute->length : len;
- return memcpy(username, attribute->value, cpylen);
+ if (!len) {
+ return username;
+ }
+
+ /* Reserve one byte for the terminator and always NUL-terminate: callers treat the result as a C string. */
+ cpylen = attribute->length < len ? attribute->length : (uint16_t)(len - 1);
+ memcpy(username, attribute->value, cpylen);
+ username[cpylen] = '\0';
+
+ return username;
}
SWITCH_DECLARE(switch_stun_packet_t *) switch_stun_packet_build_header(switch_stun_message_t type, char *id, uint8_t *buf)
@@ -836,7 +844,6 @@ SWITCH_DECLARE(switch_status_t) switch_stun_lookup(char **ip,
switch_time_t started = 0;
int funny = 0;
int size = sizeof(buf);
- int xlen = sizeof(switch_stun_packet_header_t);
switch_status_t res;
switch_assert(err);
@@ -931,7 +938,7 @@ SWITCH_DECLARE(switch_status_t) switch_stun_lookup(char **ip,
return SWITCH_STATUS_FALSE;
}
- end_buf = buf + ((sizeof(buf) > packet->header.length) ? packet->header.length : sizeof(buf));
+ end_buf = buf + ((sizeof(buf) > SWITCH_STUN_PACKET_MIN_LEN + packet->header.length) ? SWITCH_STUN_PACKET_MIN_LEN + packet->header.length : sizeof(buf));
switch_stun_packet_first_attribute(packet, attr);
switch_assert(attr);
@@ -954,13 +961,7 @@ SWITCH_DECLARE(switch_status_t) switch_stun_lookup(char **ip,
break;
}
- if (!switch_stun_packet_next_attribute(attr, end_buf)) {
- break;
- }
-
- xlen += 4 + switch_stun_attribute_padded_length(attr);
-
- } while (xlen <= packet->header.length);
+ } while (switch_stun_packet_next_attribute(attr, end_buf));
if (packet->header.type == SWITCH_STUN_BINDING_RESPONSE) {
*ip = switch_core_strdup(pool, rip);
diff --git a/tests/unit/switch_stun.c b/tests/unit/switch_stun.c
index 0948e795d9..02e3a32863 100644
--- a/tests/unit/switch_stun.c
+++ b/tests/unit/switch_stun.c
@@ -312,6 +312,169 @@ FST_TEARDOWN_END()
"a MESSAGE-INTEGRITY-SHA256 attribute after MESSAGE-INTEGRITY is tolerated");
}
FST_TEST_END()
+
+ FST_TEST_BEGIN(test_stun_get_username_terminates)
+ {
+ /* get_username must always NUL-terminate within the caller's buffer, even when the
+ attribute value is as long as or longer than the buffer: callers use the result as a C string. */
+ uint8_t abuf[128] = { 0 };
+ switch_stun_packet_attribute_t *attr = (switch_stun_packet_attribute_t *)abuf;
+ char dst[32];
+ char *ret;
+ int i;
+
+ attr->type = htons(SWITCH_STUN_ATTR_USERNAME);
+ attr->length = 64; /* host order: the accessor reads attribute->length directly, as post-parse callers do */
+ for (i = 0; i < 64; i++) {
+ attr->value[i] = 'A';
+ }
+
+ memset(dst, 'x', sizeof(dst));
+ ret = switch_stun_packet_attribute_get_username(attr, dst, sizeof(dst));
+ fst_xcheck(ret == dst, "get_username returns the destination buffer");
+ fst_xcheck(dst[sizeof(dst) - 1] == '\0', "over-long USERNAME is NUL-terminated at the last byte");
+ fst_xcheck(strlen(dst) == sizeof(dst) - 1, "over-long USERNAME is truncated to len-1");
+
+ attr->length = 5;
+ memcpy(attr->value, "abcde", 5);
+ memset(dst, 'x', sizeof(dst));
+ switch_stun_packet_attribute_get_username(attr, dst, sizeof(dst));
+ fst_xcheck(strlen(dst) == 5, "short USERNAME is copied and terminated at its own length");
+ fst_check_string_equals(dst, "abcde");
+ }
+ FST_TEST_END()
+
+ FST_TEST_BEGIN(test_stun_next_attribute_walks_whole_attributes)
+ {
+ /* Positive test: the iterator visits each whole attribute and stops exactly at the end of the
+ last one, including an attribute whose value ends on the buffer boundary. Guards against a
+ future change breaking normal iteration; it does not distinguish the header/TLV bounds fix
+ (a naive iterator passes it too). */
+ uint8_t buf[8] = { 0 };
+ switch_stun_packet_attribute_t *attr;
+ void *end = buf + sizeof(buf);
+
+ attr = (switch_stun_packet_attribute_t *)buf;
+ attr->type = htons(SWITCH_STUN_ATTR_USERNAME);
+ attr->length = 0;
+ attr = (switch_stun_packet_attribute_t *)(buf + 4);
+ attr->type = htons(SWITCH_STUN_ATTR_PRIORITY);
+ attr->length = 0;
+
+ attr = (switch_stun_packet_attribute_t *)buf;
+ fst_xcheck(switch_stun_packet_next_attribute(attr, end) != 0, "iterator advances to the second whole attribute");
+ fst_xcheck((uint8_t *)attr == buf + 4, "iterator lands exactly on the second attribute");
+ fst_xcheck(switch_stun_packet_next_attribute(attr, end) == 0, "iterator stops after the last whole attribute");
+ }
+ FST_TEST_END()
+
+ FST_TEST_BEGIN(test_stun_next_attribute_truncated_header)
+ {
+ /* After the first attribute only 2 bytes remain before end, so the next attribute's 4-byte
+ header does not fit. The iterator must confirm the header is fully in-bounds before reading
+ it and stop; reading attribute->length here would run past the buffer. The trailing bytes are
+ non-zero so the type sentinel does not stop the walk first, so the out-of-bounds read (if the
+ header check is missing) is exercised and caught under ASAN. */
+ uint8_t buf[6] = { 0 };
+ switch_stun_packet_attribute_t *attr = (switch_stun_packet_attribute_t *)buf;
+ void *end = buf + sizeof(buf);
+
+ attr->type = htons(SWITCH_STUN_ATTR_USERNAME);
+ attr->length = 0;
+ buf[4] = 0xff; /* non-zero type for the truncated trailing header */
+ buf[5] = 0xff;
+
+ fst_xcheck(switch_stun_packet_next_attribute(attr, end) == 0, "iterator stops at a truncated trailing attribute header without reading past end");
+ }
+ FST_TEST_END()
+
+ FST_TEST_BEGIN(test_stun_next_attribute_hbo)
+ {
+ /* Positive test for the _hbo iterator, which reads attribute lengths in network byte order
+ (ntohs) for callers walking raw on-the-wire packets that have not been through
+ switch_stun_packet_parse, since parse byte-swaps type and length in place: it must advance by
+ the network-order length and stop at the end. The plain macro would misread these
+ network-order lengths; this confirms the _hbo variant walks normal attributes, not a bounds
+ regression. */
+ uint8_t buf[16] = { 0 };
+ switch_stun_packet_attribute_t *attr;
+ void *end = buf + sizeof(buf);
+
+ attr = (switch_stun_packet_attribute_t *)buf;
+ attr->type = htons(SWITCH_STUN_ATTR_USERNAME);
+ attr->length = htons(4); /* network order: _hbo applies ntohs, the plain macro would misread this */
+ attr = (switch_stun_packet_attribute_t *)(buf + 8);
+ attr->type = htons(SWITCH_STUN_ATTR_PRIORITY);
+ attr->length = htons(4);
+
+ attr = (switch_stun_packet_attribute_t *)buf;
+ fst_xcheck(switch_stun_packet_next_attribute_hbo(attr, end) != 0, "hbo iterator advances to the second attribute");
+ fst_xcheck((uint8_t *)attr == buf + 8, "hbo iterator lands on the second attribute using the network-order length");
+ fst_xcheck(switch_stun_packet_next_attribute_hbo(attr, end) == 0, "hbo iterator stops after the last attribute");
+ }
+ FST_TEST_END()
+
+ FST_TEST_BEGIN(test_stun_next_attribute_value_overruns)
+ {
+ /* An attribute whose 4-byte header fits before end but whose declared value extends past end
+ (more value bytes than remain) must be rejected, so the caller never reads the value out of
+ bounds. This exercises the value-length bound distinctly from a truncated header. */
+ uint8_t buf[12] = { 0 };
+ switch_stun_packet_attribute_t *attr;
+ void *end = buf + 10; /* ends inside the second attribute's declared value */
+
+ attr = (switch_stun_packet_attribute_t *)buf;
+ attr->type = htons(SWITCH_STUN_ATTR_USERNAME);
+ attr->length = 0; /* host order: empty leading attribute */
+ attr = (switch_stun_packet_attribute_t *)(buf + 4);
+ attr->type = htons(SWITCH_STUN_ATTR_PRIORITY);
+ attr->length = 4; /* value would occupy buf[8..11], past end (buf+10) */
+
+ attr = (switch_stun_packet_attribute_t *)buf;
+ fst_xcheck(switch_stun_packet_next_attribute(attr, end) == 0, "attribute whose value overruns end is rejected");
+ }
+ FST_TEST_END()
+
+ FST_TEST_BEGIN(test_stun_walk_reaches_trailing_attribute)
+ {
+ /* Positive test: parse + walk over a well-formed multi-attribute packet reaches every attribute,
+ including a trailing one whose value ends on the last byte of the message. end_buf is computed
+ here rather than taken from switch_stun_lookup or handle_ice, so how those callers derive it is
+ not covered. */
+ uint8_t buf[512] = { 0 };
+ switch_stun_packet_t *packet;
+ switch_stun_packet_attribute_t *attr;
+ void *end_buf;
+ int count;
+
+ packet = switch_stun_packet_build_header(SWITCH_STUN_BINDING_RESPONSE, NULL, buf);
+
+ attr = (switch_stun_packet_attribute_t *)packet->first_attribute;
+ attr->type = htons(SWITCH_STUN_ATTR_USERNAME);
+ attr->length = htons(4);
+ memcpy(attr->value, "abcd", 4);
+
+ attr = (switch_stun_packet_attribute_t *)(packet->first_attribute + 8);
+ attr->type = htons(SWITCH_STUN_ATTR_USERNAME);
+ attr->length = htons(4);
+ memcpy(attr->value, "efgh", 4);
+
+ packet->header.length = htons(8 + 8);
+
+ packet = switch_stun_packet_parse(buf, SWITCH_STUN_PACKET_MIN_LEN + 8 + 8);
+ fst_requires(packet != NULL);
+
+ /* Same end_buf the iterator's callers use: the 20-byte header plus the attribute section. */
+ end_buf = buf + SWITCH_STUN_PACKET_MIN_LEN + packet->header.length;
+
+ switch_stun_packet_first_attribute(packet, attr);
+ count = 1;
+ while (switch_stun_packet_next_attribute(attr, end_buf)) {
+ count++;
+ }
+ fst_xcheck(count == 2, "iterator reaches the trailing attribute in the last bytes of the message");
+ }
+ FST_TEST_END()
}
FST_SUITE_END()
}