Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/internal.c
Original file line number Diff line number Diff line change
Expand Up @@ -3915,6 +3915,14 @@ static int GetNameListRaw(byte* idList, word32* idListSz,
return WS_BAD_ARGUMENT;
}

/* A peer name list arrives via GetStringRef with its exact wire size,
* which may include a trailing comma (e.g. "aes128-cbc,"). Trim it so the
* final name is not folded together with the comma and mis-parsed as
* ID_UNKNOWN. Mirrors AlgoListSz, which does this for local lists. */
if (nameListSz > 0 && nameList[nameListSz - 1] == ',') {
nameListSz--;
}

/*
* The strings we want are now in the bounds of the message, and the
* length of the list. Find the commas, or end of list, and then decode
Expand Down Expand Up @@ -4709,17 +4717,13 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx)
/* Languages - Client to Server, skip */
if (ret == WS_SUCCESS) {
WLOG(WS_LOG_DEBUG, "DKI: Languages - Client to Server");
ret = GetUint32(&skipSz, buf, len, &begin);
if (ret == WS_SUCCESS)
begin += skipSz;
ret = GetSkip(buf, len, &begin);
}

/* Languages - Server to Client, skip */
if (ret == WS_SUCCESS) {
WLOG(WS_LOG_DEBUG, "DKI: Languages - Server to Client");
ret = GetUint32(&skipSz, buf, len, &begin);
if (ret == WS_SUCCESS)
begin += skipSz;
ret = GetSkip(buf, len, &begin);
}

/* First KEX Packet Follows */
Expand Down
96 changes: 90 additions & 6 deletions tests/regress.c
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ static word32 AppendUint32(byte* buf, word32 bufSz, word32 idx, word32 value)
return idx;
}

static word32 ReadUint32(const byte* buf)
{
return ((word32)buf[0] << 24) | ((word32)buf[1] << 16) |
((word32)buf[2] << 8) | (word32)buf[3];
}

static word32 AppendData(byte* buf, word32 bufSz, word32 idx,
const byte* data, word32 dataSz)
{
Expand Down Expand Up @@ -394,12 +400,6 @@ typedef struct {
word32 steps;
} KexReplyRunResult;

static word32 ReadUint32(const byte* buf)
{
return ((word32)buf[0] << 24) | ((word32)buf[1] << 16) |
((word32)buf[2] << 8) | (word32)buf[3];
}

static int ReadStringRef(word32* strSz, const byte** str,
const byte* buf, word32 len, word32* idx)
{
Expand Down Expand Up @@ -2688,6 +2688,88 @@ static void TestKexInitReservedNonZeroRejected(void)
wolfSSH_CTX_free(ctx);
}

/* A peer name list may legally end with a trailing comma. GetNameListRaw
* used to fold that comma into the final name, yielding ID_UNKNOWN and a
* failed negotiation. Build a KEXINIT whose KEX list ends in a comma and
* assert the algorithm still negotiates. */
static void TestKexInitTrailingComma(void)
{
WOLFSSH_CTX* ctx;
WOLFSSH* ssh;
byte payload[512];
word32 payloadSz;
word32 idx = 0;

ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
AssertNotNull(ctx);
ssh = wolfSSH_new(ctx);
AssertNotNull(ssh);
AssertIntEQ(wolfSSH_SetAlgoListKex(ssh, FPF_KEX_GOOD), WS_SUCCESS);
AssertIntEQ(wolfSSH_SetAlgoListKey(ssh, FPF_KEY_GOOD), WS_SUCCESS);

/* KEX list carries a trailing comma. */
payloadSz = BuildKexInitPayload(ssh, FPF_KEX_GOOD ",", FPF_KEY_GOOD,
0, payload, (word32)sizeof(payload));

/* The tail (host key/send) errors on this bare ssh, but negotiation
* runs first and records the result in handshake->kexId. */
(void)wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx);

AssertNotNull(ssh->handshake);
AssertIntEQ(ssh->handshake->kexId, ID_ECDH_SHA2_NISTP256);

wolfSSH_free(ssh);
wolfSSH_CTX_free(ctx);
}

/* The two KEXINIT language name-lists were skipped with an unchecked
* begin += skipSz, so a bogus length could wrap begin back into the payload
* and let a malformed packet parse as valid. GetSkip now bounds the declared
* length against the remaining payload; an overlong language length must be
* rejected. */
static void TestKexInitLanguageLengthOverflow(void)
{
WOLFSSH_CTX* ctx;
WOLFSSH* ssh;
byte payload[512];
word32 payloadSz;
word32 idx = 0;
word32 off;
int i;

ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
AssertNotNull(ctx);
ssh = wolfSSH_new(ctx);
AssertNotNull(ssh);
AssertIntEQ(wolfSSH_SetAlgoListKex(ssh, FPF_KEX_GOOD), WS_SUCCESS);
AssertIntEQ(wolfSSH_SetAlgoListKey(ssh, FPF_KEY_GOOD), WS_SUCCESS);

payloadSz = BuildKexInitPayload(ssh, FPF_KEX_GOOD, FPF_KEY_GOOD,
0, payload, (word32)sizeof(payload));

/* Walk past the cookie and the eight algorithm name-lists to reach the
* first (client-to-server) language length field. */
off = COOKIE_SZ;
for (i = 0; i < 8; i++)
off += UINT32_SZ + ReadUint32(payload + off);

/* 0xFFFFFFFC = 2^32 - 4. After reading this 4-byte length the parser is
* 4 bytes past 'off', so the old unchecked begin += skipSz wraps begin
* back to 'off' itself -- in bounds, not past the buffer. The buggy
* parser then keeps going from the wrong offset and reads a non-zero
* reserved field, returning WS_PARSE_E. The checked GetSkip instead
* rejects the overlong length outright with WS_BUFFER_E, which is what
* locks the fix: the pre-fix code returns a different (non-buffer)
* error here. */
(void)AppendUint32(payload, (word32)sizeof(payload), off, 0xFFFFFFFCu);

AssertIntEQ(wolfSSH_TestDoKexInit(ssh, payload, payloadSz, &idx),
WS_BUFFER_E);

wolfSSH_free(ssh);
wolfSSH_CTX_free(ctx);
}

#if !defined(WOLFSSH_NO_AES_CBC) && !defined(WOLFSSH_NO_AES_CTR) \
&& !defined(WOLFSSH_NO_HMAC_SHA1) && !defined(WOLFSSH_NO_HMAC_SHA2_256)
static void TestIndependentAlgoNegotiation(void)
Expand Down Expand Up @@ -3987,6 +4069,8 @@ int main(int argc, char** argv)
&& !defined(WOLFSSH_NO_RSA_SHA2_256)
TestFirstPacketFollows();
TestKexInitReservedNonZeroRejected();
TestKexInitTrailingComma();
TestKexInitLanguageLengthOverflow();
TestDoKexInitRejectsWhenPeerIsKeying();
#endif
#if !defined(WOLFSSH_NO_ECDH_SHA2_NISTP256) && !defined(WOLFSSH_NO_RSA) \
Expand Down
Loading