Skip to content

Commit 4cf61a1

Browse files
yosuke-wolfsslejohnstown
authored andcommitted
Bound server-side inbound SFTP request size in wolfSSH_SFTP_read
1 parent 1efd647 commit 4cf61a1

3 files changed

Lines changed: 279 additions & 1 deletion

File tree

src/wolfsftp.c

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,25 @@ static int SFTP_ParseAttributes_buffer(WOLFSSH* ssh, WS_SFTP_FILEATRB* atr,
408408
static WS_SFTPNAME* wolfSSH_SFTPNAME_new(void* heap);
409409

410410

411+
/* Returns WS_SUCCESS if a server-side inbound SFTP message body of the given
412+
* size is acceptable, WS_BUFFER_E otherwise. The largest legitimate request a
413+
* server receives is a WRITE carrying WOLFSSH_MAX_SFTP_RW bytes of file data
414+
* plus the handle, offset, and length framing, so the body must be positive
415+
* and no larger than WOLFSSH_MAX_SFTP_PACKET. Bounding this before allocation
416+
* stops an authenticated client from declaring an arbitrarily large length.
417+
* Only defined when it has a caller: the server receive loop or the test hook;
418+
* a client-only build without WOLFSSH_TEST_INTERNAL leaves it unused. */
419+
#if !defined(NO_WOLFSSH_SERVER) || defined(WOLFSSH_TEST_INTERNAL)
420+
static INLINE int SFTP_CheckRecvSz(int sz)
421+
{
422+
if (sz <= 0 || sz > WOLFSSH_MAX_SFTP_PACKET) {
423+
return WS_BUFFER_E;
424+
}
425+
return WS_SUCCESS;
426+
}
427+
#endif
428+
429+
411430
/* A few errors are OK to get. They are a notice rather that a fault.
412431
* return TRUE if ssh->error is one of the following: */
413432
static INLINE int NoticeError(WOLFSSH* ssh)
@@ -663,6 +682,16 @@ int wolfSSH_TestSftpStallPending(WOLFSSH* ssh, word32 count)
663682
ssh->testSftpStallPending = count;
664683
return WS_SUCCESS;
665684
}
685+
686+
687+
/* Exposes the server-side received-packet size bound applied in
688+
* wolfSSH_SFTP_read() so the unit tests can exercise the boundary between the
689+
* largest legal inbound SFTP message and an over-large peer-declared length.
690+
* Returns WS_SUCCESS when the size is accepted, WS_BUFFER_E otherwise. */
691+
int wolfSSH_TestSftpRecvSizeCheck(int sz)
692+
{
693+
return SFTP_CheckRecvSz(sz);
694+
}
666695
#endif
667696

668697

@@ -1562,7 +1591,24 @@ int wolfSSH_SFTP_read(WOLFSSH* ssh)
15621591
* packet expected to come. */
15631592
ret = SFTP_GetHeader(ssh, (word32*)&state->reqId,
15641593
&state->type, &state->buffer);
1565-
if (ret <= 0) {
1594+
if (SFTP_CheckRecvSz(ret) != WS_SUCCESS) {
1595+
/* A positive ret is a genuine over-large declared length; log
1596+
* it as such. */
1597+
if (ret > 0) {
1598+
WLOG(WS_LOG_SFTP,
1599+
"Received SFTP packet size out of bounds");
1600+
}
1601+
/* If no lower layer left a reason (ssh->error == 0), the header
1602+
* parsed but the declared body length is invalid - over the
1603+
* cap, zero, or underflowed to negative - so record WS_BUFFER_E.
1604+
* This keeps the caller from seeing ret == WS_FATAL_ERROR with
1605+
* ssh->error == 0 and misclassifying it as a clean close via the
1606+
* channel-EOF fallback. Genuine header-read failures set
1607+
* ssh->error themselves (WS_WANT_READ to retry, WS_EOF on close,
1608+
* or a transport error) and are left untouched. */
1609+
if (ssh->error == WS_SUCCESS) {
1610+
ssh->error = WS_BUFFER_E;
1611+
}
15661612
return WS_FATAL_ERROR;
15671613
}
15681614
if (wolfSSH_SFTP_buffer_create(ssh, &state->buffer, ret) !=

tests/unit.c

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@
6363
#include <wolfssh/certman.h>
6464
#endif
6565

66+
#ifdef WOLFSSH_SFTP
67+
#include <wolfssh/wolfsftp.h>
68+
#endif
69+
6670
#if defined(WOLFSSH_SCP) && !defined(WOLFSSH_SCP_USER_CALLBACKS) && \
6771
!defined(NO_FILESYSTEM) && !defined(WOLFSSL_NUCLEUS) && \
6872
!defined(_WIN32) && !defined(WOLFSSH_ZEPHYR)
@@ -4362,6 +4366,203 @@ static int test_DoUserAuthRequestRsaCert(void)
43624366

43634367
#endif /* !WOLFSSH_NO_RSA && !WOLFSSH_NO_SSH_RSA_SHA1 */
43644368

4369+
#ifdef WOLFSSH_SFTP
4370+
/* Property test for the server-side received-packet size bound applied in
4371+
* wolfSSH_SFTP_read(). A non-positive size or one above the largest legal
4372+
* inbound SFTP message must be rejected before any buffer is allocated;
4373+
* in-range sizes are accepted. */
4374+
static int test_SftpRecvSizeBound(void)
4375+
{
4376+
int testVals[7];
4377+
int expectOk;
4378+
int ret;
4379+
int i;
4380+
int maxWriteBody;
4381+
4382+
testVals[0] = -1; /* underflow / error sentinel */
4383+
testVals[1] = 0; /* empty body */
4384+
testVals[2] = 1; /* smallest accepted body */
4385+
testVals[3] = WOLFSSH_MAX_SFTP_PACKET; /* upper bound, accepted */
4386+
testVals[4] = WOLFSSH_MAX_SFTP_PACKET + 1; /* just over the bound */
4387+
testVals[5] = 0x40000000; /* the ~1 GB attack value */
4388+
testVals[6] = 0x7FFFFFFF; /* INT32_MAX */
4389+
4390+
for (i = 0; i < (int)(sizeof(testVals) / sizeof(testVals[0])); i++) {
4391+
ret = wolfSSH_TestSftpRecvSizeCheck(testVals[i]);
4392+
4393+
expectOk = (testVals[i] > 0 &&
4394+
testVals[i] <= WOLFSSH_MAX_SFTP_PACKET);
4395+
4396+
if (expectOk) {
4397+
if (ret != WS_SUCCESS)
4398+
return -900 - i;
4399+
}
4400+
else {
4401+
if (ret == WS_SUCCESS)
4402+
return -920 - i;
4403+
}
4404+
}
4405+
4406+
/* The bound value must be large enough to admit a real maximum-size WRITE,
4407+
* otherwise legitimate large writes would be silently rejected. A WRITE
4408+
* body is a handle string (length prefix + up to WOLFSSH_MAX_HANDLE), an
4409+
* 8-byte file offset, then a data string (length prefix + up to
4410+
* WOLFSSH_MAX_SFTP_RW). Guards against a future shrink of the bound (e.g.
4411+
* to WOLFSSH_MAX_SFTP_RECV alone). */
4412+
maxWriteBody = UINT32_SZ + WOLFSSH_MAX_HANDLE /* handle string */
4413+
+ (2 * UINT32_SZ) /* 64-bit offset */
4414+
+ UINT32_SZ + WOLFSSH_MAX_SFTP_RW; /* data string */
4415+
if (wolfSSH_TestSftpRecvSizeCheck(maxWriteBody) != WS_SUCCESS)
4416+
return -930;
4417+
4418+
return 0;
4419+
}
4420+
4421+
/* IORecv mock that reports no data is available yet. ReceiveData maps this to
4422+
* WS_WANT_READ, letting the receive loop reach its body-read state without a
4423+
* live socket. */
4424+
static int RecvAlwaysWantRead(WOLFSSH* ssh, void* data, word32 sz, void* ctx)
4425+
{
4426+
WOLFSSH_UNUSED(ssh);
4427+
WOLFSSH_UNUSED(data);
4428+
WOLFSSH_UNUSED(sz);
4429+
WOLFSSH_UNUSED(ctx);
4430+
return WS_CBIO_ERR_WANT_READ;
4431+
}
4432+
4433+
/* Drives a single crafted SFTP request header through wolfSSH_SFTP_read() on a
4434+
* fresh server session. The on-wire length field is set to len and the message
4435+
* type to WRITE; only the 9-byte header is staged in the channel input buffer,
4436+
* never a body. ioRecv, when non-NULL, stands in for the socket if the loop is
4437+
* admitted and reaches its body-read state. On success fills *outRet with the
4438+
* wolfSSH_SFTP_read() return and *outErr with ssh->error, and returns 0; on a
4439+
* setup failure returns a negative sentinel. DiscardIoSend absorbs the
4440+
* window-adjust emitted after the header is consumed, and acceptState is
4441+
* advanced so that adjust is allowed on the bare session. */
4442+
static int SftpRecvDriveHeader(word32 len, WS_CallbackIORecv ioRecv,
4443+
int* outRet, int* outErr)
4444+
{
4445+
WOLFSSH_CTX* ctx = NULL;
4446+
WOLFSSH* ssh = NULL;
4447+
WOLFSSH_CHANNEL* ch = NULL;
4448+
int result = 0;
4449+
byte header[WOLFSSH_SFTP_HEADER];
4450+
4451+
*outRet = WS_SUCCESS;
4452+
*outErr = WS_SUCCESS;
4453+
4454+
header[0] = (byte)((len >> 24) & 0xFF);
4455+
header[1] = (byte)((len >> 16) & 0xFF);
4456+
header[2] = (byte)((len >> 8) & 0xFF);
4457+
header[3] = (byte)( len & 0xFF);
4458+
header[LENGTH_SZ] = WOLFSSH_FTP_WRITE; /* message type */
4459+
header[LENGTH_SZ + MSG_ID_SZ + 0] = 0x00; /* request id = 1 */
4460+
header[LENGTH_SZ + MSG_ID_SZ + 1] = 0x00;
4461+
header[LENGTH_SZ + MSG_ID_SZ + 2] = 0x00;
4462+
header[LENGTH_SZ + MSG_ID_SZ + 3] = 0x01;
4463+
4464+
ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
4465+
if (ctx == NULL)
4466+
return -1000;
4467+
wolfSSH_SetIOSend(ctx, DiscardIoSend);
4468+
if (ioRecv != NULL)
4469+
wolfSSH_SetIORecv(ctx, ioRecv);
4470+
4471+
ssh = wolfSSH_new(ctx);
4472+
if (ssh == NULL) { result = -1001; goto done; }
4473+
/* Allow MSGID_CHANNEL_WINDOW_ADJUST on this bare session. */
4474+
ssh->acceptState = ACCEPT_SERVER_USERAUTH_SENT;
4475+
4476+
ch = ChannelNew(ssh, ID_CHANTYPE_SESSION, 1024, 1024);
4477+
if (ch == NULL) { result = -1002; goto done; }
4478+
if (ChannelAppend(ssh, ch) != WS_SUCCESS) {
4479+
ChannelDelete(ch, ssh->ctx->heap);
4480+
result = -1003;
4481+
goto done;
4482+
}
4483+
4484+
if (wolfSSH_TestChannelPutData(ssh->channelList, header,
4485+
(word32)sizeof(header)) != WS_SUCCESS) {
4486+
result = -1004;
4487+
goto done;
4488+
}
4489+
4490+
*outRet = wolfSSH_SFTP_read(ssh);
4491+
*outErr = wolfSSH_get_error(ssh);
4492+
4493+
done:
4494+
wolfSSH_free(ssh);
4495+
wolfSSH_CTX_free(ctx);
4496+
return result;
4497+
}
4498+
4499+
/* End-to-end check that the receive loop itself rejects an invalid declared
4500+
* inbound length, not just the SFTP_CheckRecvSz helper in isolation. Drives two
4501+
* crafted headers whose decoded body length the bound must refuse: one well
4502+
* past WOLFSSH_MAX_SFTP_PACKET and one of zero. Each must return WS_FATAL_ERROR
4503+
* with ssh->error == WS_BUFFER_E and allocate no body buffer. The zero case
4504+
* also guards against ssh->error being left at 0, which a caller could misread
4505+
* as a clean channel close. */
4506+
static int test_SftpRecvSizeBoundIntegration(void)
4507+
{
4508+
word32 lens[2];
4509+
int rc;
4510+
int ret;
4511+
int err;
4512+
int i;
4513+
4514+
/* On-wire length field counts the type byte, request id, and body. */
4515+
lens[0] = (word32)WOLFSSH_MAX_SFTP_PACKET + 100 + MSG_ID_SZ + UINT32_SZ;
4516+
lens[1] = (word32)(MSG_ID_SZ + UINT32_SZ); /* decoded body length 0 */
4517+
4518+
for (i = 0; i < (int)(sizeof(lens) / sizeof(lens[0])); i++) {
4519+
rc = SftpRecvDriveHeader(lens[i], NULL, &ret, &err);
4520+
if (rc != 0)
4521+
return rc;
4522+
if (ret != WS_FATAL_ERROR)
4523+
return -945 - (i * 10);
4524+
if (err != WS_BUFFER_E)
4525+
return -946 - (i * 10);
4526+
}
4527+
4528+
return 0;
4529+
}
4530+
4531+
/* End-to-end check that a legitimate maximum-size WRITE is NOT rejected by the
4532+
* server bound. The decoded body length equals the largest real WRITE body
4533+
* (handle + 8-byte offset + max data). The bound must admit it: the loop
4534+
* allocates the body buffer and then asks for the body that has not arrived, so
4535+
* the call returns WS_FATAL_ERROR with ssh->error == WS_WANT_READ (a benign
4536+
* retry) rather than WS_BUFFER_E (a bound rejection). RecvAlwaysWantRead stands
4537+
* in for a non-blocking socket with no data yet. */
4538+
static int test_SftpRecvSizeBoundAccept(void)
4539+
{
4540+
word32 len;
4541+
int rc;
4542+
int ret;
4543+
int err;
4544+
4545+
/* Largest legitimate WRITE body, matching test_SftpRecvSizeBound, plus the
4546+
* type byte and request id the on-wire length field carries. */
4547+
len = (word32)(UINT32_SZ + WOLFSSH_MAX_HANDLE
4548+
+ (2 * UINT32_SZ)
4549+
+ UINT32_SZ + WOLFSSH_MAX_SFTP_RW)
4550+
+ MSG_ID_SZ + UINT32_SZ;
4551+
4552+
rc = SftpRecvDriveHeader(len, RecvAlwaysWantRead, &ret, &err);
4553+
if (rc != 0)
4554+
return rc;
4555+
if (ret != WS_FATAL_ERROR)
4556+
return -955;
4557+
/* WS_WANT_READ confirms the bound admitted the body and the loop is waiting
4558+
* on it; WS_BUFFER_E here would mean a legitimate max WRITE was rejected. */
4559+
if (err != WS_WANT_READ)
4560+
return -956;
4561+
4562+
return 0;
4563+
}
4564+
#endif /* WOLFSSH_SFTP */
4565+
43654566
#endif /* WOLFSSH_TEST_INTERNAL */
43664567

43674568
/* Error Code And Message Test */
@@ -4540,6 +4741,22 @@ int wolfSSH_UnitTest(int argc, char** argv)
45404741
printf("IdentifyAsn1Key: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED"));
45414742
testResult = testResult || unitResult;
45424743

4744+
#ifdef WOLFSSH_SFTP
4745+
unitResult = test_SftpRecvSizeBound();
4746+
printf("SftpRecvSizeBound: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED"));
4747+
testResult = testResult || unitResult;
4748+
4749+
unitResult = test_SftpRecvSizeBoundIntegration();
4750+
printf("SftpRecvSizeBoundIntegration: %s\n",
4751+
(unitResult == 0 ? "SUCCESS" : "FAILED"));
4752+
testResult = testResult || unitResult;
4753+
4754+
unitResult = test_SftpRecvSizeBoundAccept();
4755+
printf("SftpRecvSizeBoundAccept: %s\n",
4756+
(unitResult == 0 ? "SUCCESS" : "FAILED"));
4757+
testResult = testResult || unitResult;
4758+
#endif
4759+
45434760
#if defined(WOLFSSH_SCP) && !defined(WOLFSSH_SCP_USER_CALLBACKS) && \
45444761
!defined(NO_FILESYSTEM) && !defined(WOLFSSL_NUCLEUS) && \
45454762
!defined(_WIN32) && !defined(WOLFSSH_ZEPHYR)

wolfssh/wolfsftp.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,20 @@ struct WS_SFTPNAME {
174174
#ifndef WOLFSSH_MAX_SFTP_RECV
175175
#define WOLFSSH_MAX_SFTP_RECV 32768
176176
#endif
177+
/*
178+
* WOLFSSH_MAX_SFTP_PACKET: Upper bound on the body size of an inbound SFTP
179+
* request the server accepts in its steady-state receive loop
180+
* (wolfSSH_SFTP_read). The largest legitimate request is a WRITE carrying
181+
* WOLFSSH_MAX_SFTP_RW bytes of file data plus the handle, offset, and
182+
* length framing, so the bound is WOLFSSH_MAX_SFTP_RW plus the message
183+
* overhead allowance of WOLFSSH_MAX_SFTP_RECV. Used to reject an over-large
184+
* client-declared length before any buffer is allocated. This bound does
185+
* not apply to client-side response handling (e.g. NAME directory listings
186+
* or READ data), whose sizes are not derived from WOLFSSH_MAX_SFTP_RW.
187+
*/
188+
#ifndef WOLFSSH_MAX_SFTP_PACKET
189+
#define WOLFSSH_MAX_SFTP_PACKET (WOLFSSH_MAX_SFTP_RW + WOLFSSH_MAX_SFTP_RECV)
190+
#endif
177191

178192
/* functions for establishing a connection */
179193
WOLFSSH_API int wolfSSH_SFTP_accept(WOLFSSH* ssh);
@@ -287,6 +301,7 @@ WOLFSSH_LOCAL void wolfSSH_SFTP_ShowSizes(void);
287301
#ifdef WOLFSSH_TEST_INTERNAL
288302
WOLFSSH_API int wolfSSH_TestSftpBufferSend(WOLFSSH* ssh,
289303
byte* data, word32 sz, word32 idx);
304+
WOLFSSH_API int wolfSSH_TestSftpRecvSizeCheck(int sz);
290305
#ifndef NO_WOLFSSH_SERVER
291306
WOLFSSH_API int wolfSSH_TestSftpValidateFileHandle(WOLFSSH* ssh,
292307
const byte* handle, word32 handleSz);

0 commit comments

Comments
 (0)