Skip to content

Commit 412c428

Browse files
committed
Fix TLS ext bounds checking
1 parent 31278ee commit 412c428

7 files changed

Lines changed: 392 additions & 36 deletions

File tree

src/tls.c

Lines changed: 94 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6504,7 +6504,7 @@ static int TLSX_SessionTicket_Parse(WOLFSSL* ssl, const byte* input,
65046504
return ret;
65056505
}
65066506

6507-
WOLFSSL_LOCAL SessionTicket* TLSX_SessionTicket_Create(word32 lifetime,
6507+
WOLFSSL_TEST_VIS SessionTicket* TLSX_SessionTicket_Create(word32 lifetime,
65086508
byte* data, word16 size, void* heap)
65096509
{
65106510
SessionTicket* ticket = (SessionTicket*)XMALLOC(sizeof(SessionTicket),
@@ -6525,7 +6525,7 @@ WOLFSSL_LOCAL SessionTicket* TLSX_SessionTicket_Create(word32 lifetime,
65256525

65266526
return ticket;
65276527
}
6528-
WOLFSSL_LOCAL void TLSX_SessionTicket_Free(SessionTicket* ticket, void* heap)
6528+
WOLFSSL_TEST_VIS void TLSX_SessionTicket_Free(SessionTicket* ticket, void* heap)
65296529
{
65306530
if (ticket) {
65316531
XFREE(ticket->data, heap, DYNAMIC_TYPE_TLSX);
@@ -14879,9 +14879,27 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType,
1487914879
{
1488014880
int ret = 0;
1488114881
TLSX* extension;
14882-
word16 length = 0;
14882+
/* Use a word32 accumulator so that an extension whose contribution
14883+
* pushes the running total past 0xFFFF is detected rather than
14884+
* silently wrapped (the TLS extensions block length prefix on the
14885+
* wire is a 2-byte field). Callees that take a word16* accumulator
14886+
* are invoked via a per-iteration shim (`cbShim`) and their delta
14887+
* is added back into the word32 total.
14888+
*
14889+
* MAINTAINER NOTE: do NOT pass &length to any *_GET_SIZE function
14890+
* that expects a `word16*` out-parameter -- that would be a type
14891+
* mismatch (UB) and would silently bypass the overflow detection
14892+
* below. When adding a new extension case, either:
14893+
* - use `length += FOO_GET_SIZE(...)` when the helper returns a
14894+
* word16 by value, or
14895+
* - use the cbShim pattern: `cbShim = 0; ret = FOO_GET_SIZE(...,
14896+
* &cbShim); length += cbShim;`
14897+
*/
14898+
word32 length = 0;
14899+
word16 cbShim = 0;
1488314900
byte isRequest = (msgType == client_hello ||
1488414901
msgType == certificate_request);
14902+
(void)cbShim;
1488514903

1488614904
while ((extension = list)) {
1488714905
list = extension->next;
@@ -14965,23 +14983,31 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType,
1496514983
#endif
1496614984
#if defined(HAVE_ENCRYPT_THEN_MAC) && !defined(WOLFSSL_AEAD_ONLY)
1496714985
case TLSX_ENCRYPT_THEN_MAC:
14968-
ret = ETM_GET_SIZE(msgType, &length);
14986+
cbShim = 0;
14987+
ret = ETM_GET_SIZE(msgType, &cbShim);
14988+
length += cbShim;
1496914989
break;
1497014990
#endif /* HAVE_ENCRYPT_THEN_MAC */
1497114991

1497214992
#if defined(WOLFSSL_TLS13) || !defined(WOLFSSL_NO_TLS12) || !defined(NO_OLD_TLS)
1497314993
#if defined(HAVE_SESSION_TICKET) || !defined(NO_PSK)
1497414994
case TLSX_PRE_SHARED_KEY:
14995+
cbShim = 0;
1497514996
ret = PSK_GET_SIZE((PreSharedKey*)extension->data, msgType,
14976-
&length);
14997+
&cbShim);
14998+
length += cbShim;
1497714999
break;
1497815000
#ifdef WOLFSSL_TLS13
1497915001
case TLSX_PSK_KEY_EXCHANGE_MODES:
14980-
ret = PKM_GET_SIZE((byte)extension->val, msgType, &length);
15002+
cbShim = 0;
15003+
ret = PKM_GET_SIZE((byte)extension->val, msgType, &cbShim);
15004+
length += cbShim;
1498115005
break;
1498215006
#ifdef WOLFSSL_CERT_WITH_EXTERN_PSK
1498315007
case TLSX_CERT_WITH_EXTERN_PSK:
14984-
ret = PSK_WITH_CERT_GET_SIZE(msgType, &length);
15008+
cbShim = 0;
15009+
ret = PSK_WITH_CERT_GET_SIZE(msgType, &cbShim);
15010+
length += cbShim;
1498515011
break;
1498615012
#endif
1498715013
#endif
@@ -14993,22 +15019,30 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType,
1499315019

1499415020
#ifdef WOLFSSL_TLS13
1499515021
case TLSX_SUPPORTED_VERSIONS:
14996-
ret = SV_GET_SIZE(extension->data, msgType, &length);
15022+
cbShim = 0;
15023+
ret = SV_GET_SIZE(extension->data, msgType, &cbShim);
15024+
length += cbShim;
1499715025
break;
1499815026

1499915027
case TLSX_COOKIE:
15000-
ret = CKE_GET_SIZE((Cookie*)extension->data, msgType, &length);
15028+
cbShim = 0;
15029+
ret = CKE_GET_SIZE((Cookie*)extension->data, msgType, &cbShim);
15030+
length += cbShim;
1500115031
break;
1500215032

1500315033
#ifdef WOLFSSL_EARLY_DATA
1500415034
case TLSX_EARLY_DATA:
15005-
ret = EDI_GET_SIZE(msgType, &length);
15035+
cbShim = 0;
15036+
ret = EDI_GET_SIZE(msgType, &cbShim);
15037+
length += cbShim;
1500615038
break;
1500715039
#endif
1500815040

1500915041
#ifdef WOLFSSL_POST_HANDSHAKE_AUTH
1501015042
case TLSX_POST_HANDSHAKE_AUTH:
15011-
ret = PHA_GET_SIZE(msgType, &length);
15043+
cbShim = 0;
15044+
ret = PHA_GET_SIZE(msgType, &cbShim);
15045+
length += cbShim;
1501215046
break;
1501315047
#endif
1501415048

@@ -15061,12 +15095,26 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType,
1506115095
break;
1506215096
}
1506315097

15098+
/* Early exit: stop accumulating as soon as the running total
15099+
* cannot possibly fit the 2-byte wire length. Check *before*
15100+
* marking the extension as processed so the semaphore is not
15101+
* left in an inconsistent state on the error path. */
15102+
if (length > WOLFSSL_MAX_16BIT) {
15103+
WOLFSSL_MSG("TLSX_GetSize extension length exceeds word16");
15104+
return BUFFER_E;
15105+
}
15106+
1506415107
/* marks the extension as processed so ctx level */
1506515108
/* extensions don't overlap with ssl level ones. */
1506615109
TURN_ON(semaphore, TLSX_ToSemaphore((word16)extension->type));
1506715110
}
1506815111

15069-
*pLength += length;
15112+
if ((word32)*pLength + length > WOLFSSL_MAX_16BIT) {
15113+
WOLFSSL_MSG("TLSX_GetSize total extensions length exceeds word16");
15114+
return BUFFER_E;
15115+
}
15116+
15117+
*pLength += (word16)length;
1507015118

1507115119
return ret;
1507215120
}
@@ -15079,6 +15127,7 @@ static int TLSX_Write(TLSX* list, byte* output, byte* semaphore,
1507915127
TLSX* extension;
1508015128
word16 offset = 0;
1508115129
word16 length_offset = 0;
15130+
word32 prevOffset;
1508215131
byte isRequest = (msgType == client_hello ||
1508315132
msgType == certificate_request);
1508415133

@@ -15093,6 +15142,10 @@ static int TLSX_Write(TLSX* list, byte* output, byte* semaphore,
1509315142
if (!IS_OFF(semaphore, TLSX_ToSemaphore((word16)extension->type)))
1509415143
continue; /* skip! */
1509515144

15145+
/* Snapshot offset to detect word16 wrap within this iteration;
15146+
* see matching comment in TLSX_GetSize. */
15147+
prevOffset = offset;
15148+
1509615149
/* writes extension type. */
1509715150
c16toa((word16)extension->type, output + offset);
1509815151
offset += HELLO_EXT_TYPE_SZ + OPAQUE16_LEN;
@@ -15326,9 +15379,24 @@ static int TLSX_Write(TLSX* list, byte* output, byte* semaphore,
1532615379
/* if we encountered an error propagate it */
1532715380
if (ret != 0)
1532815381
break;
15382+
15383+
if (offset <= prevOffset) {
15384+
WOLFSSL_MSG("TLSX_Write extension length exceeds word16");
15385+
return BUFFER_E;
15386+
}
1532915387
}
1533015388

15331-
*pOffset += offset;
15389+
/* Only validate and commit the aggregate offset when the loop
15390+
* completed without error; on the error path, leave *pOffset
15391+
* unchanged and return the original failure reason so callers
15392+
* see the real error instead of a masking BUFFER_E. */
15393+
if (ret == 0) {
15394+
if ((word32)*pOffset + (word32)offset > WOLFSSL_MAX_16BIT) {
15395+
WOLFSSL_MSG("TLSX_Write total extensions length exceeds word16");
15396+
return BUFFER_E;
15397+
}
15398+
*pOffset += offset;
15399+
}
1533215400

1533315401
return ret;
1533415402
}
@@ -16432,6 +16500,13 @@ int TLSX_GetRequestSize(WOLFSSL* ssl, byte msgType, word32* pLength)
1643216500
}
1643316501
#endif
1643416502

16503+
/* The TLS extensions block length prefix is a 2-byte field, so any
16504+
* accumulated total above 0xFFFF must be rejected rather than silently
16505+
* truncating and producing a short, malformed handshake message. */
16506+
if (length > (word16)(WOLFSSL_MAX_16BIT - OPAQUE16_LEN)) {
16507+
WOLFSSL_MSG("TLSX_GetRequestSize extensions exceed word16");
16508+
return BUFFER_E;
16509+
}
1643516510
if (length)
1643616511
length += OPAQUE16_LEN; /* for total length storage. */
1643716512

@@ -16635,6 +16710,12 @@ int TLSX_WriteRequest(WOLFSSL* ssl, byte* output, byte msgType, word32* pOffset)
1663516710
#endif
1663616711
#endif
1663716712

16713+
/* Wrap detection for the TLSX_Write calls above is handled inside
16714+
* TLSX_Write itself: any iteration that would push the local word16
16715+
* offset past 0xFFFF returns BUFFER_E so we never reach here with a
16716+
* truncated value. The TLS extensions block length prefix on the
16717+
* wire is a 2-byte field, matching this invariant. */
16718+
1663816719
if (offset > OPAQUE16_LEN || msgType != client_hello)
1663916720
c16toa(offset - OPAQUE16_LEN, output); /* extensions length */
1664016721

src/tls13.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3279,6 +3279,10 @@ int BuildTls13Message(WOLFSSL* ssl, byte* output, int outSz, const byte* input,
32793279

32803280
WOLFSSL_ENTER("BuildTls13Message");
32813281

3282+
if (ssl == NULL) {
3283+
return BAD_FUNC_ARG;
3284+
}
3285+
32823286
#ifdef WOLFSSL_ASYNC_CRYPT
32833287
ret = WC_NO_PENDING_E;
32843288
if (asyncOkay) {

tests/api.c

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10578,6 +10578,144 @@ static int test_tls_ext_duplicate(void)
1057810578
return EXPECT_RESULT();
1057910579
}
1058010580

10581+
/* Regression test: TLSX extension size accumulation must not silently wrap
10582+
* the internal word16 accumulator. Prior to the fix, a single extension
10583+
* whose size (plus the 4-byte header) pushes the running total past 0xFFFF
10584+
* caused TLSX_GetSize / TLSX_Write to return a truncated length, which
10585+
* in turn led to undersized buffer writes. The on-wire extensions block
10586+
* length is a 2-byte field per RFC 8446 Section 4.2, so the correct
10587+
* behavior is to return BUFFER_E rather than wrap. */
10588+
static int test_tls_ext_word16_overflow(void)
10589+
{
10590+
EXPECT_DECLS;
10591+
#if defined(HAVE_SESSION_TICKET) && !defined(NO_WOLFSSL_CLIENT) && \
10592+
!defined(NO_TLS)
10593+
WOLFSSL_CTX* ctx = NULL;
10594+
WOLFSSL* ssl = NULL;
10595+
SessionTicket* ticket = NULL;
10596+
byte* big = NULL;
10597+
/* Size chosen so that 4 (ext header) + size > 0xFFFF. */
10598+
const word16 bigSz = 0xFFFE;
10599+
word32 length = 0;
10600+
10601+
big = (byte*)XMALLOC(bigSz, NULL, DYNAMIC_TYPE_TMP_BUFFER);
10602+
ExpectNotNull(big);
10603+
if (big != NULL)
10604+
XMEMSET(big, 0xA5, bigSz);
10605+
10606+
ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method()));
10607+
ExpectNotNull(ssl = wolfSSL_new(ctx));
10608+
10609+
/* Build an oversized SessionTicket extension directly on the ssl
10610+
* extension list. Going via the public API is not enough here because
10611+
* wolfSSL_set_SessionTicket clamps to word16 without creating the
10612+
* TLSX entry; the TLSX path is what exercises the accumulator. */
10613+
if (EXPECT_SUCCESS()) {
10614+
ticket = TLSX_SessionTicket_Create(0, big, bigSz, ssl->heap);
10615+
ExpectNotNull(ticket);
10616+
}
10617+
if (EXPECT_SUCCESS()) {
10618+
ExpectIntEQ(TLSX_UseSessionTicket(&ssl->extensions, ticket, ssl->heap),
10619+
WOLFSSL_SUCCESS);
10620+
/* TLSX_UseSessionTicket takes ownership on success. */
10621+
ticket = NULL;
10622+
}
10623+
10624+
/* TLSX_GetRequestSize must refuse to encode: 4-byte ext header +
10625+
* 0xFFFE payload + 2-byte block length prefix = 0x10004, which does
10626+
* not fit in a word16 wire length. Expect BUFFER_E, not a silently
10627+
* wrapped small value. */
10628+
if (EXPECT_SUCCESS()) {
10629+
int ret = TLSX_GetRequestSize(ssl, client_hello, &length);
10630+
ExpectIntEQ(ret, BUFFER_E);
10631+
}
10632+
10633+
if (ticket != NULL)
10634+
TLSX_SessionTicket_Free(ticket, ssl->heap);
10635+
wolfSSL_free(ssl);
10636+
wolfSSL_CTX_free(ctx);
10637+
XFREE(big, NULL, DYNAMIC_TYPE_TMP_BUFFER);
10638+
ssl = NULL;
10639+
ctx = NULL;
10640+
big = NULL;
10641+
10642+
/* Boundary case: construct a SessionTicket extension sized so that the
10643+
* total extensions length in TLSX_GetRequestSize is exactly
10644+
* WOLFSSL_MAX_16BIT - OPAQUE16_LEN (0xFFFD) *before* the OPAQUE16_LEN
10645+
* block-prefix adjustment, which must succeed. This pins the `>`
10646+
* comparison in the overflow check -- mutating it to `>=` would
10647+
* incorrectly reject this valid case and fail this test. */
10648+
{
10649+
WOLFSSL_CTX* ctx2 = NULL;
10650+
WOLFSSL* ssl2 = NULL;
10651+
SessionTicket* ticket2 = NULL;
10652+
byte* buf = NULL;
10653+
word32 baseLen = 0;
10654+
word32 baseInternal = 0;
10655+
word32 tickSz = 0;
10656+
/* TLSX_GetRequestSize rejects when internal sum > 0xFFFD. */
10657+
const word32 target = (word32)WOLFSSL_MAX_16BIT - (word32)OPAQUE16_LEN;
10658+
/* Session ticket extension contributes: type (2) + len (2) + size. */
10659+
const word32 extHdr = (word32)HELLO_EXT_TYPE_SZ
10660+
+ (word32)OPAQUE16_LEN;
10661+
int ret;
10662+
10663+
ExpectNotNull(ctx2 = wolfSSL_CTX_new(wolfSSLv23_client_method()));
10664+
ExpectNotNull(ssl2 = wolfSSL_new(ctx2));
10665+
10666+
/* Measure baseline length with no session ticket extension. The
10667+
* returned value already includes the 2-byte block-length prefix
10668+
* when nonzero; strip it to get the raw internal sum. */
10669+
if (EXPECT_SUCCESS()) {
10670+
baseLen = 0;
10671+
ret = TLSX_GetRequestSize(ssl2, client_hello, &baseLen);
10672+
ExpectIntEQ(ret, 0);
10673+
baseInternal = (baseLen > 0)
10674+
? baseLen - (word32)OPAQUE16_LEN : 0;
10675+
}
10676+
10677+
/* Target: baseInternal + extHdr + tickSz == 0xFFFD. */
10678+
if (EXPECT_SUCCESS() && baseInternal + extHdr < target) {
10679+
tickSz = target - baseInternal - extHdr;
10680+
}
10681+
10682+
if (EXPECT_SUCCESS() && tickSz > 0 && tickSz <= WOLFSSL_MAX_16BIT) {
10683+
buf = (byte*)XMALLOC(tickSz, NULL, DYNAMIC_TYPE_TMP_BUFFER);
10684+
ExpectNotNull(buf);
10685+
if (buf != NULL)
10686+
XMEMSET(buf, 0x5A, tickSz);
10687+
}
10688+
10689+
if (EXPECT_SUCCESS() && buf != NULL) {
10690+
ticket2 = TLSX_SessionTicket_Create(0, buf, (word16)tickSz,
10691+
ssl2->heap);
10692+
ExpectNotNull(ticket2);
10693+
}
10694+
if (EXPECT_SUCCESS() && ticket2 != NULL) {
10695+
ExpectIntEQ(TLSX_UseSessionTicket(&ssl2->extensions, ticket2,
10696+
ssl2->heap), WOLFSSL_SUCCESS);
10697+
ticket2 = NULL;
10698+
}
10699+
10700+
/* Exact boundary: internal sum == 0xFFFD must succeed, and the
10701+
* final returned length is 0xFFFD + OPAQUE16_LEN == 0xFFFF. */
10702+
if (EXPECT_SUCCESS()) {
10703+
word32 lenBoundary = 0;
10704+
ret = TLSX_GetRequestSize(ssl2, client_hello, &lenBoundary);
10705+
ExpectIntEQ(ret, 0);
10706+
ExpectIntEQ(lenBoundary, (word32)WOLFSSL_MAX_16BIT);
10707+
}
10708+
10709+
if (ticket2 != NULL)
10710+
TLSX_SessionTicket_Free(ticket2, ssl2->heap);
10711+
wolfSSL_free(ssl2);
10712+
wolfSSL_CTX_free(ctx2);
10713+
XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER);
10714+
}
10715+
#endif
10716+
return EXPECT_RESULT();
10717+
}
10718+
1058110719

1058210720
/* Test TLS connection abort when legacy version field indicates TLS 1.3 or
1058310721
* higher. Based on test_tls_ext_duplicate() but with legacy version modified
@@ -37373,6 +37511,7 @@ TEST_CASE testCases[] = {
3737337511
TEST_DECL(test_wolfSSL_SCR_Reconnect),
3737437512
TEST_DECL(test_wolfSSL_SCR_check_enabled),
3737537513
TEST_DECL(test_tls_ext_duplicate),
37514+
TEST_DECL(test_tls_ext_word16_overflow),
3737637515
TEST_DECL(test_tls_bad_legacy_version),
3737737516
#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH)
3737837517
#if defined(HAVE_IO_TESTS_DEPENDENCIES)

0 commit comments

Comments
 (0)