Uploaded image for project: 'Qt'
  1. Qt
  2. QTBUG-70691

QWebSocketServer security issues, denial of service

    XMLWordPrintable

Details

    • Bug
    • Resolution: Done
    • P2: Important
    • 5.15
    • 5.11.2, 5.12.0 Beta 3
    • WebSockets
    • None
    • All
    • db472ab205b37f44cb2d65ad861152cb9f48f2e8

    Description

      The current QWebSocketServer class has security issues.

      The class uses internally a QSslServer (or a QTcpServer if no ssl), the QSslServer works as usual, it emits a newConnection signal when a new ssl/tcp connection is made.

       

      1) First bug

      The QWebSocketServer connects to this signal and does the following:

      void QWebSocketServerPrivate::handleConnection(QTcpSocket *pTcpSocket) const
      {
          if (Q_LIKELY(pTcpSocket)) {
              // Use a queued connection because a QSslSocket needs the event loop to process incoming
              // data. If not queued, data is incomplete when handshakeReceived is called.
              QObjectPrivate::connect(pTcpSocket, &QTcpSocket::readyRead,
                                      this, &QWebSocketServerPrivate::handshakeReceived,
                                      Qt::QueuedConnection);
              if (pTcpSocket->canReadLine()) {
                  // We received some data! We must emit now to be sure that handshakeReceived is called
                  // since the data could have been received before the signal and slot was connected.
                  emit pTcpSocket->readyRead();
              }
              QObjectPrivate::connect(pTcpSocket, &QTcpSocket::disconnected,
                                      this, &QWebSocketServerPrivate::onSocketDisconnected);
          }
      }
      

      It only registers for readyRead & disconnected events.
      This opens a denial of service vulnerability, as an attacker could for example, sends nothing after the TCP Connection. The TCP Connection would stay there indefinitely, the attacker could open a lot of TCP connections like this until the exhaustion of all sockets of the server.
      This could cause a denial of service, very easy to achieve.

      As a solution, the websocketserver should implement a disconnection timeout if no data has been sent in a certain amount of time. This amount of time should be modifiable by a class member function.

       

      2) Second bug

      A second issue exists, that opens another denial of service vulnerability (still due to websocket handshake incorrect handling).

      When the tcpsocket emits the readyRead signal, the following is done:

      void QWebSocketServerPrivate::handshakeReceived()
      {
          if (Q_UNLIKELY(!currentSender)) {
              return;
          }
          QTcpSocket *pTcpSocket = qobject_cast<QTcpSocket*>(currentSender->sender);
          if (Q_UNLIKELY(!pTcpSocket)) {
              return;
          }
          //When using Google Chrome the handshake in received in two parts.
          //Therefore, the readyRead signal is emitted twice.
          //This is a guard against the BEAST attack.
          //See: https://www.imperialviolet.org/2012/01/15/beastfollowup.html
          //For Safari, the handshake is delivered at once
          //FIXME: For FireFox, the readyRead signal is never emitted
          //This is a bug in FireFox (see https://bugzilla.mozilla.org/show_bug.cgi?id=594502)
          if (!pTcpSocket->canReadLine()) {
              return;
          }
          disconnect(pTcpSocket, &QTcpSocket::readyRead,
                     this, &QWebSocketServerPrivate::handshakeReceived);
          Q_Q(QWebSocketServer);
          bool success = false;
          bool isSecure = (m_secureMode == SecureMode);
          if (m_pendingConnections.length() >= maxPendingConnections()) {
              pTcpSocket->close();
              pTcpSocket->deleteLater();
              setError(QWebSocketProtocol::CloseCodeAbnormalDisconnection,
                       QWebSocketServer::tr("Too many pending connections."));
              return;
          }
          QWebSocketHandshakeRequest request(pTcpSocket->peerPort(), isSecure);
          QTextStream textStream(pTcpSocket);
          request.readHandshake(textStream, MAX_HEADERLINE_LENGTH, MAX_HEADERLINES);
          if (request.isValid()) {
              QWebSocketCorsAuthenticator corsAuthenticator(request.origin());
              Q_EMIT q->originAuthenticationRequired(&corsAuthenticator);
              QWebSocketHandshakeResponse response(request,
                                                   m_serverName,
                                                   corsAuthenticator.allowed(),
                                                   supportedVersions(),
                                                   supportedProtocols(),
                                                   supportedExtensions());
              if (response.isValid()) {
                  QTextStream httpStream(pTcpSocket);
                  httpStream << response;
                  httpStream.flush();
                  if (response.canUpgrade()) {
                      QWebSocket *pWebSocket = QWebSocketPrivate::upgradeFrom(pTcpSocket,
                                                                              request,
                                                                              response);
                      if (pWebSocket) {
                          addPendingConnection(pWebSocket);
                          Q_EMIT q->newConnection();
                          success = true;
                      } else {
                          setError(QWebSocketProtocol::CloseCodeAbnormalDisconnection,
                                   QWebSocketServer::tr("Upgrade to WebSocket failed."));
                      }
                  }
                  else {
                      setError(response.error(), response.errorString());
                  }
              } else {
                  setError(QWebSocketProtocol::CloseCodeProtocolError,
                           QWebSocketServer::tr("Invalid response received."));
              }
          }
          if (!success) {
              pTcpSocket->close();
          }
      }
      

      If !pTcpSocket->canReadLine() (ie if the tcpsocket buffer, doesn't contain any '\n') then the handshake data is not processed.

      Again an attacker could decide to send any amount of data (for example several gigabytes) that doesn't contain any '\n'. Of course the attacker could continue until the exhaustion of all virtual memory of the server, causing a server crash and a denial of service.

       

      As a solution, the websocketserver should implement a max handshake data size, and disconnect the tcpsocket if this size is reached. This amount of  data should be modifiable by a class member function.

       

      Conclusion:

      Currently nothing can be done to prevent this as the qwebsocketserver doesn't expose internal qtcpsocket before the websocket handshake completes.

      This makes the qt websocket server not useable for production code.

      Attachments

        No reviews matched the request. Check your Options in the drop-down menu of this sections header.

        Activity

          People

            enstone Franck Dude
            enstone Franck Dude
            Votes:
            2 Vote for this issue
            Watchers:
            5 Start watching this issue

            Dates

              Created:
              Updated:
              Resolved:

              Gerrit Reviews

                There are no open Gerrit changes