@@ -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
@@ -10722,20 +10731,63 @@ int DoProtoId(WOLFSSH* ssh)
1072210731 int ret;
1072310732 word32 idSz;
1072410733 byte* eol;
10725- byte SSH_PROTO_EOL_SZ = 1;
10734+ byte eolSz;
10735+ int allowBanner;
1072610736
10727- if ( (ret = GetInputText(ssh, &eol)) < 0) {
10728- WLOG(WS_LOG_DEBUG, "get input text failed");
10729- return ret;
10730- }
10737+ /*
10738+ * RFC 4253 Section 4.2: The server MAY send other lines of data before
10739+ * the version string. The client MUST send the version string first
10740+ * with no preceding data. So pre-version banner lines are accepted
10741+ * only when this endpoint is the client reading the server's
10742+ * identification. A wolfSSH server reading a client's identification
10743+ * rejects any non-"SSH-" line. The 255-byte per-line cap is enforced
10744+ * by GetInputLine, which also returns WS_SOCKET_ERROR_E if the peer
10745+ * stops sending before a complete line arrives. The number of banner
10746+ * lines is also bounded by WOLFSSH_MAX_BANNER_LINES so a peer cannot
10747+ * stall the connection by dribbling well-formed banner lines forever
10748+ * even when the embedder has not configured a connect timeout.
10749+ */
10750+ allowBanner = (ssh->ctx->side == WOLFSSH_ENDPOINT_CLIENT);
1073110751
10732- if (eol == NULL) {
10733- WLOG(WS_LOG_DEBUG, "invalid EOL");
10734- return WS_VERSION_E;
10735- }
10752+ do {
10753+ ret = GetInputLine(ssh, &eol);
10754+ if (ret < 0) {
10755+ WLOG(WS_LOG_DEBUG, "get input line failed");
10756+ return ret;
10757+ }
1073610758
10737- if (WSTRNCASECMP((char*)ssh->inputBuffer.buffer,
10738- ssh->ctx->sshProtoIdStr, SSH_PROTO_SZ) == 0) {
10759+ if (ssh->inputBuffer.length >= 4
10760+ && WSTRNCASECMP((char*)ssh->inputBuffer.buffer, "SSH-", 4) == 0)
10761+ break;
10762+
10763+ if (!allowBanner) {
10764+ WLOG(WS_LOG_DEBUG, "non-SSH line from peer");
10765+ return WS_VERSION_E;
10766+ }
10767+ if (++ssh->handshake->bannerLines > WOLFSSH_MAX_BANNER_LINES) {
10768+ WLOG(WS_LOG_DEBUG, "too many banner lines");
10769+ return WS_VERSION_E;
10770+ }
10771+
10772+ /* Banner line: log without the terminator and advance past it.
10773+ * The next GetInputLine compacts so the next line starts at
10774+ * buffer[0]. Logged at debug level since the bytes are
10775+ * peer-controlled and may contain control characters. */
10776+ WLOG(WS_LOG_DEBUG, "peer banner: %.*s",
10777+ (int)(eol - ssh->inputBuffer.buffer),
10778+ ssh->inputBuffer.buffer);
10779+ ssh->inputBuffer.idx +=
10780+ (word32)(eol - ssh->inputBuffer.buffer)
10781+ + ((*eol == '\r') ? 2 : 1);
10782+ } while (1);
10783+
10784+ /* eol points at the start of the version line's terminator: '\r' for
10785+ * CRLF or '\n' for LF only. */
10786+ eolSz = (*eol == '\r') ? 2 : 1;
10787+
10788+ if (ssh->inputBuffer.length >= SSH_PROTO_SZ
10789+ && WSTRNCASECMP((char*)ssh->inputBuffer.buffer,
10790+ ssh->ctx->sshProtoIdStr, SSH_PROTO_SZ) == 0) {
1073910791
1074010792 if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER)
1074110793 ssh->clientState = CLIENT_VERSION_DONE;
@@ -10751,9 +10803,6 @@ int DoProtoId(WOLFSSH* ssh)
1075110803 ssh->clientOpenSSH = 1;
1075210804 }
1075310805
10754- if (*eol == '\r') {
10755- SSH_PROTO_EOL_SZ++;
10756- }
1075710806 *eol = 0;
1075810807
1075910808 idSz = (word32)WSTRLEN((char*)ssh->inputBuffer.buffer);
@@ -10769,7 +10818,7 @@ int DoProtoId(WOLFSSH* ssh)
1076910818 ssh->peerProtoIdSz = idSz + LENGTH_SZ;
1077010819 }
1077110820
10772- ssh->inputBuffer.idx += idSz + SSH_PROTO_EOL_SZ ;
10821+ ssh->inputBuffer.idx += idSz + eolSz ;
1077310822
1077410823 ShrinkBuffer(&ssh->inputBuffer, 0);
1077510824
@@ -17869,6 +17918,11 @@ void AddAssign64(word32* addend1, word32 addend2)
1786917918
1787017919#ifdef WOLFSSH_TEST_INTERNAL
1787117920
17921+ int wolfSSH_TestDoProtoId(WOLFSSH* ssh)
17922+ {
17923+ return DoProtoId(ssh);
17924+ }
17925+
1787217926int wolfSSH_TestIsMessageAllowed(WOLFSSH* ssh, byte msg, byte state)
1787317927{
1787417928 return IsMessageAllowed(ssh, msg, state);
0 commit comments