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 ); diff --git a/src/serverrpc.cpp b/src/serverrpc.cpp index 05bb54be62..015f3a85f9 100644 --- a/src/serverrpc.cpp +++ b/src/serverrpc.cpp @@ -319,7 +319,21 @@ CServerRpc::CServerRpc ( CServer* pServer, CRpcServer* pRpcServer, QObject* pare return; } - pServer->SetWelcomeMessage ( jsonWelcomeMessage.toString() ); + // Check the decoded string, not the request bytes: \uXXXX escapes and multi-byte + // UTF-8 both make the encoded form longer than the string it produces. The bound is + // MAX_LEN_CHAT_TEXT because that is what CServer::SetWelcomeMessage truncates to; + // accepting more here would report success and then silently discard the excess. + const QString strWelcomeMessage = jsonWelcomeMessage.toString(); + + if ( strWelcomeMessage.length() > MAX_LEN_CHAT_TEXT ) + { + response["error"] = CRpcServer::CreateJsonRpcError ( + CRpcServer::iErrInvalidParams, + QString ( "Invalid params: welcomeMessage exceeds maximum length of %1 characters" ).arg ( MAX_LEN_CHAT_TEXT ) ); + return; + } + + pServer->SetWelcomeMessage ( strWelcomeMessage ); response["result"] = "ok"; } );