Skip to content

Commit 4dcaf6e

Browse files
ejohnstownpadelsbach
authored andcommitted
DoProtoId: skip pre-version banner lines
1. Client skips non-SSH lines (RFC 4253 4.2); server rejects them 2. 255-byte per-line cap and 10-line cap (WOLFSSH_MAX_BANNER_LINES) 3. 28 test vectors plus scripted-IO mock for WANT_READ resumption Issue: F-606
1 parent 444aa0f commit 4dcaf6e

3 files changed

Lines changed: 520 additions & 57 deletions

File tree

src/internal.c

Lines changed: 111 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3359,59 +3359,68 @@ static int ReceiveData(WOLFSSH* ssh, byte* buf, word32 sz)
33593359
}
33603360

33613361

3362-
static int GetInputText(WOLFSSH* ssh, byte** pEol)
3362+
/* Reads one CRLF- or LF-terminated line into ssh->inputBuffer. On success,
3363+
* *pEol points to the start of the terminator ('\r' for CRLF, '\n' for LF
3364+
* only) so the caller can compute line length and terminator size.
3365+
*
3366+
* Per RFC 4253 Section 4.2, the SSH version line is at most 255 octets
3367+
* including the terminator. The same cap is applied to pre-version banner
3368+
* lines: a line longer than that returns WS_VERSION_E. */
3369+
static int GetInputLine(WOLFSSH* ssh, byte** pEol)
33633370
{
3364-
int gotLine = 0;
3365-
int inSz = 255;
3371+
int inSz;
33663372
int in;
3367-
char *eol = NULL;
3368-
3369-
if (GrowBuffer(&ssh->inputBuffer, inSz) < 0)
3373+
char* lf;
3374+
byte* buffer;
3375+
3376+
/* Compact so any unconsumed data (e.g., the remainder of a prior
3377+
* banner-line read) starts at buffer[0]. GrowBuffer(0) shifts the
3378+
* data when length > 0, and is preferred over ShrinkBuffer here
3379+
* because ShrinkBuffer skips the shift when data exceeds
3380+
* STATIC_BUFFER_LEN. */
3381+
if (GrowBuffer(&ssh->inputBuffer, 0) < 0)
33703382
return WS_MEMORY_E;
33713383

33723384
do {
3373-
in = ReceiveData(ssh,
3374-
ssh->inputBuffer.buffer + ssh->inputBuffer.length, inSz);
3375-
3376-
if (in == -1) {
3377-
return WS_SOCKET_ERROR_E;
3378-
}
3379-
3380-
if (in == WS_WANT_READ) {
3381-
return WS_WANT_READ;
3382-
}
3383-
3384-
if (in > inSz) {
3385-
return WS_RECV_OVERFLOW_E;
3386-
}
3387-
3388-
ssh->inputBuffer.length += in;
3389-
inSz -= in;
3390-
3391-
eol = WSTRNSTR((const char*)ssh->inputBuffer.buffer, "\r\n",
3392-
ssh->inputBuffer.length);
3393-
3394-
/* section 4.2 in RFC 4253 states that can be lenient on the CR for
3395-
* interop with older or undocumented versions of SSH */
3396-
if (!eol) {
3397-
WLOG(WS_LOG_DEBUG, "Checking for old version of protocol exchange");
3398-
eol = WSTRNSTR((const char*)ssh->inputBuffer.buffer, "\n",
3399-
ssh->inputBuffer.length);
3385+
buffer = ssh->inputBuffer.buffer;
3386+
lf = WSTRNSTR((const char*)buffer, "\n", ssh->inputBuffer.length);
3387+
if (lf != NULL) {
3388+
/* eol points to the start of the terminator: back up onto a
3389+
* preceding CR if present, otherwise leave it on the LF. Be
3390+
* lenient on the CR per RFC 4253 Section 4.2. */
3391+
if ((byte*)lf > buffer && lf[-1] == '\r')
3392+
lf--;
3393+
if (pEol)
3394+
*pEol = (byte*)lf;
3395+
return WS_SUCCESS;
34003396
}
34013397

3402-
if (eol)
3403-
gotLine = 1;
3398+
/* 255-byte per-line cap: includes the terminator. If the buffer is
3399+
* already at the cap with no LF, the line is too long. */
3400+
if (ssh->inputBuffer.length >= WOLFSSH_PROTOID_LIMIT)
3401+
return WS_VERSION_E;
34043402

3405-
} while (!gotLine && inSz);
3403+
inSz = WOLFSSH_PROTOID_LIMIT - (int)ssh->inputBuffer.length;
3404+
if (GrowBuffer(&ssh->inputBuffer, inSz) != WS_SUCCESS)
3405+
return WS_MEMORY_E;
34063406

3407-
if (pEol)
3408-
*pEol = (byte*)eol;
3407+
in = ReceiveData(ssh,
3408+
ssh->inputBuffer.buffer + ssh->inputBuffer.length,
3409+
(word32)inSz);
34093410

3410-
if (!gotLine) {
3411-
return WS_VERSION_E;
3412-
}
3411+
if (in == WS_WANT_READ)
3412+
return WS_WANT_READ;
3413+
/* Any other non-positive return is treated as a connection failure:
3414+
* ReceiveData maps WS_CBIO_ERR_CONN_CLOSE to -1, and a clean EOF
3415+
* (in == 0) mid-version-exchange means the peer hung up before
3416+
* sending a complete identification line. */
3417+
if (in <= 0)
3418+
return WS_SOCKET_ERROR_E;
3419+
if (in > inSz)
3420+
return WS_RECV_OVERFLOW_E;
34133421

3414-
return WS_SUCCESS;
3422+
ssh->inputBuffer.length += (word32)in;
3423+
} while (1);
34153424
}
34163425

34173426

@@ -10733,20 +10742,63 @@ int DoProtoId(WOLFSSH* ssh)
1073310742
int ret;
1073410743
word32 idSz;
1073510744
byte* eol;
10736-
byte SSH_PROTO_EOL_SZ = 1;
10745+
byte eolSz;
10746+
int allowBanner;
1073710747

10738-
if ( (ret = GetInputText(ssh, &eol)) < 0) {
10739-
WLOG(WS_LOG_DEBUG, "get input text failed");
10740-
return ret;
10741-
}
10748+
/*
10749+
* RFC 4253 Section 4.2: The server MAY send other lines of data before
10750+
* the version string. The client MUST send the version string first
10751+
* with no preceding data. So pre-version banner lines are accepted
10752+
* only when this endpoint is the client reading the server's
10753+
* identification. A wolfSSH server reading a client's identification
10754+
* rejects any non-"SSH-" line. The 255-byte per-line cap is enforced
10755+
* by GetInputLine, which also returns WS_SOCKET_ERROR_E if the peer
10756+
* stops sending before a complete line arrives. The number of banner
10757+
* lines is also bounded by WOLFSSH_MAX_BANNER_LINES so a peer cannot
10758+
* stall the connection by dribbling well-formed banner lines forever
10759+
* even when the embedder has not configured a connect timeout.
10760+
*/
10761+
allowBanner = (ssh->ctx->side == WOLFSSH_ENDPOINT_CLIENT);
1074210762

10743-
if (eol == NULL) {
10744-
WLOG(WS_LOG_DEBUG, "invalid EOL");
10745-
return WS_VERSION_E;
10746-
}
10763+
do {
10764+
ret = GetInputLine(ssh, &eol);
10765+
if (ret < 0) {
10766+
WLOG(WS_LOG_DEBUG, "get input line failed");
10767+
return ret;
10768+
}
1074710769

10748-
if (WSTRNCASECMP((char*)ssh->inputBuffer.buffer,
10749-
ssh->ctx->sshProtoIdStr, SSH_PROTO_SZ) == 0) {
10770+
if (ssh->inputBuffer.length >= 4
10771+
&& WSTRNCASECMP((char*)ssh->inputBuffer.buffer, "SSH-", 4) == 0)
10772+
break;
10773+
10774+
if (!allowBanner) {
10775+
WLOG(WS_LOG_DEBUG, "non-SSH line from peer");
10776+
return WS_VERSION_E;
10777+
}
10778+
if (++ssh->handshake->bannerLines > WOLFSSH_MAX_BANNER_LINES) {
10779+
WLOG(WS_LOG_DEBUG, "too many banner lines");
10780+
return WS_VERSION_E;
10781+
}
10782+
10783+
/* Banner line: log without the terminator and advance past it.
10784+
* The next GetInputLine compacts so the next line starts at
10785+
* buffer[0]. Logged at debug level since the bytes are
10786+
* peer-controlled and may contain control characters. */
10787+
WLOG(WS_LOG_DEBUG, "peer banner: %.*s",
10788+
(int)(eol - ssh->inputBuffer.buffer),
10789+
ssh->inputBuffer.buffer);
10790+
ssh->inputBuffer.idx +=
10791+
(word32)(eol - ssh->inputBuffer.buffer)
10792+
+ ((*eol == '\r') ? 2 : 1);
10793+
} while (1);
10794+
10795+
/* eol points at the start of the version line's terminator: '\r' for
10796+
* CRLF or '\n' for LF only. */
10797+
eolSz = (*eol == '\r') ? 2 : 1;
10798+
10799+
if (ssh->inputBuffer.length >= SSH_PROTO_SZ
10800+
&& WSTRNCASECMP((char*)ssh->inputBuffer.buffer,
10801+
ssh->ctx->sshProtoIdStr, SSH_PROTO_SZ) == 0) {
1075010802

1075110803
if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER)
1075210804
ssh->clientState = CLIENT_VERSION_DONE;
@@ -10762,9 +10814,6 @@ int DoProtoId(WOLFSSH* ssh)
1076210814
ssh->clientOpenSSH = 1;
1076310815
}
1076410816

10765-
if (*eol == '\r') {
10766-
SSH_PROTO_EOL_SZ++;
10767-
}
1076810817
*eol = 0;
1076910818

1077010819
idSz = (word32)WSTRLEN((char*)ssh->inputBuffer.buffer);
@@ -10780,7 +10829,7 @@ int DoProtoId(WOLFSSH* ssh)
1078010829
ssh->peerProtoIdSz = idSz + LENGTH_SZ;
1078110830
}
1078210831

10783-
ssh->inputBuffer.idx += idSz + SSH_PROTO_EOL_SZ;
10832+
ssh->inputBuffer.idx += idSz + eolSz;
1078410833

1078510834
ShrinkBuffer(&ssh->inputBuffer, 0);
1078610835

@@ -17880,6 +17929,11 @@ void AddAssign64(word32* addend1, word32 addend2)
1788017929

1788117930
#ifdef WOLFSSH_TEST_INTERNAL
1788217931

17932+
int wolfSSH_TestDoProtoId(WOLFSSH* ssh)
17933+
{
17934+
return DoProtoId(ssh);
17935+
}
17936+
1788317937
int wolfSSH_TestIsMessageAllowed(WOLFSSH* ssh, byte msg, byte state)
1788417938
{
1788517939
return IsMessageAllowed(ssh, msg, state);

0 commit comments

Comments
 (0)