CVE-2023-32081 (Java, published 2023-05-12T14:15Z)
Description: value: Vert.x STOMP is a vert.x implementation of the STOMP specification that provides a STOMP server and client. From versions 3.1.0 until 3.9.16 and 4.0.0 until 4.4.2, a Vert.x STOMP server processes client STOMP frames without checking that the client send an initial CONNECT frame replied with a successful CONNECTED frame. The client can subscribe to a destination or publish message without prior authentication. Any Vert.x STOMP server configured with an authentication handler is impacted. The issue is patched in Vert.x 3.9.16 and 4.4.2. There are no trivial workarounds.
Severity: nan (CVSS v3 base score: 6.5)
Weakness: CWE-287 \u2014 Improper Authentication
When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
Repository: https://github.com/vert-x3/vertx-stomp
Files changed:
src/main/java/io/vertx/ext/stomp/DefaultConnectHandler.java
src/main/java/io/vertx/ext/stomp/StompServerConnection.java
src/main/java/io/vertx/ext/stomp/impl/DefaultStompHandler.java
src/main/java/io/vertx/ext/stomp/impl/StompServerImpl.java
src/test/java/io/vertx/ext/stomp/impl/SecuredServerConnectionTest.java
Commit message: STOMP server process client frames that would not send initially a connect frame

A Vert.x STOMP server processes client STOMP frames without checking that the client send an initial CONNECT frame replied with a successful CONNECTED frame. The client can subscribe to a destination or publish message without prior authentication. Any Vert.x STOMP server configured with an authentication handler is impacted.

Fixes CVE-2023-32081


Vulnerable code:
  private Handler<ServerFrame> connectHandler = new DefaultConnectHandler();
          StompServerConnection connection = new StompServerTCPConnectionImpl(socket, this, writingFrameHandler);
              .handler(frame -> stomp.handle(new ServerFrameImpl(frame, connection)));
      StompServerConnection connection = new StompServerWebSocketConnectionImpl(socket, this, writingFrameHandler);
          .handler(frame -> stomp.handle(new ServerFrameImpl(frame, connection)));
    server = StompServer.create(vertx, new StompServerOptions().setSecured(true))
        .handler(StompServerHandler.create(vertx).authProvider(provider));
    server.listen().onComplete(context.asyncAssertSuccess());
    context.assertTrue(buffer.toString().contains("CONNECTED"));

Fixed code:


  private Handler<ServerFrame> connectHandler;
    this.connectHandler = new DefaultConnectHandler();
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;
          AtomicBoolean connected = new AtomicBoolean();
          AtomicBoolean firstFrame = new AtomicBoolean();
          StompServerConnection connection = new StompServerTCPConnectionImpl(socket, this, frame -> {
            if (frame.frame().getCommand() == Command.CONNECTED) {
              connected.set(true);
            }
            Handler<ServerFrame> h = writingFrameHandler;
            if (h != null) {
              h.handle(frame);
            }
          });
              .handler(frame -> {
                if (frame.getCommand() == Command.CONNECT || frame.getCommand() == Command.STOMP) {
                  if (firstFrame.compareAndSet(false, true)) {
                    stomp.handle(new ServerFrameImpl(frame, connection));
                  } else {
                    connection.write(Frames.createErrorFrame("Already connected", Collections.emptyMap(), ""));
                    connection.close();
                  }
                } else if (connected.get()) {
                  stomp.handle(new ServerFrameImpl(frame, connection));
                } else {
                  connection.write(Frames.createErrorFrame("Not connected", Collections.emptyMap(), ""));
                  connection.close();
                }
              });
      AtomicBoolean connected = new AtomicBoolean();
      AtomicBoolean firstFrame = new AtomicBoolean();
      StompServerConnection connection = new StompServerWebSocketConnectionImpl(socket, this, frame -> {
        if (frame.frame().getCommand() == Command.CONNECTED  || frame.frame().getCommand() == Command.STOMP) {
          connected.set(true);
        }
        Handler<ServerFrame> h = writingFrameHandler;
        if (h != null) {
          h.handle(frame);
        }
      });
          .handler(frame -> {
            if (frame.getCommand() == Command.CONNECT) {
              if (firstFrame.compareAndSet(false, true)) {
                stomp.handle(new ServerFrameImpl(frame, connection));
              } else {
                connection.write(Frames.createErrorFrame("Already connected", Collections.emptyMap(), ""));
                connection.close();
              }
            } else if (connected.get()) {
              stomp.handle(new ServerFrameImpl(frame, connection));
            } else {
              connection.write(Frames.createErrorFrame("Not connected", Collections.emptyMap(), ""));
              connection.close();
            }
          });
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.net.NetClient;
import java.util.Arrays;


  private HttpServer wsServer;
  private HttpClient wsClient;
  private StompClient client;
    server = StompServer.create(vertx, new StompServerOptions()
        .setSecured(true)
        .setWebsocketBridge(true)
        .setWebsocketPath("/stomp"))
      .handler(StompServerHandler.create(vertx).authProvider(provider));
    server.listen(StompServerOptions.DEFAULT_STOMP_PORT).onComplete(context.asyncAssertSuccess());
    wsServer = vertx.createHttpServer(new HttpServerOptions().setWebSocketSubProtocols(Arrays.asList("v10.stomp", "v11.stomp")))
      .webSocketHandler(server.webSocketHandler());
    wsServer.listen(8080).onComplete(context.asyncAssertSuccess());
    wsClient = vertx.createHttpClient();
    client = StompClient.create(vertx, new StompClientOptions().setLogin("admin").setPasscode("admin"));
    context.assertTrue(buffer.toString().contains("CONNECTED"), "Was expected <" + buffer.toString() + "> to contain 'CONNECTED'");
  @Test
  public void testTCPClientMustBeConnected(TestContext context) {
    Async async = context.async();
    NetClient client = vertx.createNetClient();
    testClientMustBeConnected(context, v -> {
      client.connect(server.actualPort(), "0.0.0.0").onComplete(context.asyncAssertSuccess(so -> {
        Buffer received = Buffer.buffer();
        so.handler(received::appendBuffer);
        so.write(
          "SEND\n" +
            "destination:/test\n" +
            "\n" +
            "hello" +
            FrameParser.NULL);
        so.endHandler(v2 -> {
          context.assertTrue(received.toString().startsWith("ERROR\n"));
          async.complete();
        });
      }));
    });
  }

  @Test
  public void testWebSocketClientMustBeConnected(TestContext context) {
    Async async = context.async();
    testClientMustBeConnected(context, v -> {
      wsClient.webSocket(8080, "localhost", "/stomp").onComplete(context.asyncAssertSuccess(ws -> {
        Buffer received = Buffer.buffer();
        ws.binaryMessageHandler(received::appendBuffer);
        ws.writeBinaryMessage(
          Buffer.buffer("SEND\n" +
            "destination:/test\n" +
            "\n" +
            "hello" +
            FrameParser.NULL));
        ws.endHandler(v2 -> {
          context.assertTrue(received.toString().startsWith("ERROR\n"));
          async.complete();
        });
      }));
    });
  }

  private void testClientMustBeConnected(TestContext context, Handler<Void> cont) {
    client
      .connect(server.actualPort(), "localhost")
      .onComplete(context.asyncAssertSuccess(conn -> {
        Future<String> fut = conn.subscribe("/test", frame -> {
          context.fail("Should not receive a messsage");
        });
        fut.onComplete(context.asyncAssertSuccess(v2 -> {
          cont.handle(null);
        }));
      }));
  }