diff --git a/src/rpcserver.cpp b/src/rpcserver.cpp index 2c05c2f12d..248c971b47 100644 --- a/src/rpcserver.cpp +++ b/src/rpcserver.cpp @@ -110,6 +110,17 @@ QJsonObject CRpcServer::CreateJsonRpcErrorReply ( int code, QString message ) return object; } +// Maximum size of a single JSON-RPC request line. An unauthenticated client that +// sends data without a terminating newline is only ever consumed on a complete line +// (canReadLine()), so without a bound the received bytes accumulate in the socket read +// buffer without limit until the process is killed by the allocator. Requests larger +// than this, or unterminated data that fills the buffer, are rejected instead of held. +// The largest single request is a welcome or chat message, which CServer truncates to +// MAX_LEN_CHAT_TEXT (1600) characters and which is 9698 bytes as a compact JSON line +// when every character is escaped as \uXXXX; 16 KiB leaves 1.7x that, or room for a +// batch of 221 ordinary calls. +static constexpr int MAX_JSON_RPC_REQUEST_BYTES = 16 * 1024; + void CRpcServer::OnNewConnection() { QTcpSocket* pSocket = pTransportServer->nextPendingConnection(); @@ -120,16 +131,36 @@ void CRpcServer::OnNewConnection() qDebug() << "- JSON-RPC: received connection from:" << pSocket->peerAddress().toString(); vecClients.append ( pSocket ); - isAuthenticated[pSocket] = false; + isAuthenticated[pSocket] = false; + isDiscardingLine[pSocket] = false; + + // Bound the per-connection read buffer so unterminated input cannot exhaust memory. + pSocket->setReadBufferSize ( MAX_JSON_RPC_REQUEST_BYTES ); connect ( pSocket, &QTcpSocket::disconnected, [this, pSocket]() { qDebug() << "- JSON-RPC: connection from:" << pSocket->peerAddress().toString() << "closed"; vecClients.removeAll ( pSocket ); isAuthenticated.remove ( pSocket ); + isDiscardingLine.remove ( pSocket ); pSocket->deleteLater(); } ); connect ( pSocket, &QTcpSocket::readyRead, [this, pSocket]() { + // An oversized request was already answered with an error; the rest of its line + // is discarded here so that the connection, and the authentication bound to it, + // survive and the next request is read normally. + if ( isDiscardingLine[pSocket] ) + { + const QByteArray sPending = pSocket->peek ( pSocket->bytesAvailable() ); + const int iEndOfLine = sPending.indexOf ( '\n' ); + pSocket->read ( iEndOfLine < 0 ? sPending.size() : iEndOfLine + 1 ); + if ( iEndOfLine < 0 ) + { + return; + } + isDiscardingLine[pSocket] = false; + } + while ( pSocket->canReadLine() ) { QByteArray line = pSocket->readLine(); @@ -197,6 +228,18 @@ void CRpcServer::OnNewConnection() pSocket->disconnectFromHost(); return; } + + // A full buffer with no complete line is an oversized or unterminated request: + // answer it, then drop the bytes instead of holding them. + if ( !pSocket->canReadLine() && pSocket->bytesAvailable() >= MAX_JSON_RPC_REQUEST_BYTES ) + { + Send ( pSocket, + QJsonDocument ( CreateJsonRpcErrorReply ( + iErrParseError, + QString ( "Parse error: Request exceeds maximum size of %1 bytes" ).arg ( MAX_JSON_RPC_REQUEST_BYTES ) ) ) ); + isDiscardingLine[pSocket] = true; + pSocket->read ( pSocket->bytesAvailable() ); + } } ); } diff --git a/src/rpcserver.h b/src/rpcserver.h index cf9da2706f..0803099a56 100644 --- a/src/rpcserver.h +++ b/src/rpcserver.h @@ -94,6 +94,7 @@ class CRpcServer : public QObject // A map from method name to handler functions QMap mapMethodHandlers; QMap isAuthenticated; + QMap isDiscardingLine; QVector vecClients; void HandleApiAuth ( QTcpSocket* pSocket, const QJsonObject& params, QJsonObject& response );