Skip to content

Commit 0de4bc5

Browse files
committed
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
1 parent 5cc8222 commit 0de4bc5

File tree

5 files changed

+137
-9
lines changed

5 files changed

+137
-9
lines changed

Diff for: src/main/java/io/vertx/ext/stomp/DefaultConnectHandler.java

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
* @author <a href="http://escoffier.me">Clement Escoffier</a>
4040
*/
4141
public class DefaultConnectHandler implements Handler<ServerFrame> {
42+
4243
@Override
4344
public void handle(ServerFrame sf) {
4445
// Server negotiation

Diff for: src/main/java/io/vertx/ext/stomp/StompServerConnection.java

+1
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,5 @@ public interface StompServerConnection {
9696
* @param pingHandler the ping handler
9797
*/
9898
void configureHeartbeat(long ping, long pong, Handler<StompServerConnection> pingHandler);
99+
99100
}

Diff for: src/main/java/io/vertx/ext/stomp/impl/DefaultStompHandler.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public class DefaultStompHandler implements StompServerHandler {
7070
private final Vertx vertx;
7171
private final Context context;
7272

73-
private Handler<ServerFrame> connectHandler = new DefaultConnectHandler();
73+
private Handler<ServerFrame> connectHandler;
7474

7575
private Handler<ServerFrame> stompHandler;
7676

@@ -125,6 +125,7 @@ public DefaultStompHandler(Vertx vertx) {
125125
this.context = Vertx.currentContext();
126126
this.destinations = vertx.sharedData().getLocalMap("stomp.destinations");
127127
this.users = new ConcurrentHashMap<>();
128+
this.connectHandler = new DefaultConnectHandler();
128129
}
129130

130131
@Override

Diff for: src/main/java/io/vertx/ext/stomp/impl/StompServerImpl.java

+54-4
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
import io.vertx.core.net.NetServer;
2828
import io.vertx.ext.stomp.*;
2929

30+
import java.util.Collections;
3031
import java.util.Objects;
32+
import java.util.concurrent.atomic.AtomicBoolean;
3133

3234
/**
3335
* Default implementation of the {@link StompServer}.
@@ -109,7 +111,17 @@ public StompServer listen(int port, String host, Handler<AsyncResult<StompServer
109111
"server.");
110112
server
111113
.connectHandler(socket -> {
112-
StompServerConnection connection = new StompServerTCPConnectionImpl(socket, this, writingFrameHandler);
114+
AtomicBoolean connected = new AtomicBoolean();
115+
AtomicBoolean firstFrame = new AtomicBoolean();
116+
StompServerConnection connection = new StompServerTCPConnectionImpl(socket, this, frame -> {
117+
if (frame.frame().getCommand() == Command.CONNECTED) {
118+
connected.set(true);
119+
}
120+
Handler<ServerFrame> h = writingFrameHandler;
121+
if (h != null) {
122+
h.handle(frame);
123+
}
124+
});
113125
FrameParser parser = new FrameParser(options);
114126
socket.exceptionHandler((exception) -> {
115127
LOGGER.error("The STOMP server caught a TCP socket error - closing connection", exception);
@@ -123,7 +135,21 @@ public StompServer listen(int port, String host, Handler<AsyncResult<StompServer
123135
connection.close();
124136
}
125137
)
126-
.handler(frame -> stomp.handle(new ServerFrameImpl(frame, connection)));
138+
.handler(frame -> {
139+
if (frame.getCommand() == Command.CONNECT || frame.getCommand() == Command.STOMP) {
140+
if (firstFrame.compareAndSet(false, true)) {
141+
stomp.handle(new ServerFrameImpl(frame, connection));
142+
} else {
143+
connection.write(Frames.createErrorFrame("Already connected", Collections.emptyMap(), ""));
144+
connection.close();
145+
}
146+
} else if (connected.get()) {
147+
stomp.handle(new ServerFrameImpl(frame, connection));
148+
} else {
149+
connection.write(Frames.createErrorFrame("Not connected", Collections.emptyMap(), ""));
150+
connection.close();
151+
}
152+
});
127153
socket.handler(parser);
128154
})
129155
.listen(port, host).onComplete(ar -> {
@@ -218,7 +244,17 @@ public Handler<ServerWebSocket> webSocketHandler() {
218244
socket.reject();
219245
return;
220246
}
221-
StompServerConnection connection = new StompServerWebSocketConnectionImpl(socket, this, writingFrameHandler);
247+
AtomicBoolean connected = new AtomicBoolean();
248+
AtomicBoolean firstFrame = new AtomicBoolean();
249+
StompServerConnection connection = new StompServerWebSocketConnectionImpl(socket, this, frame -> {
250+
if (frame.frame().getCommand() == Command.CONNECTED || frame.frame().getCommand() == Command.STOMP) {
251+
connected.set(true);
252+
}
253+
Handler<ServerFrame> h = writingFrameHandler;
254+
if (h != null) {
255+
h.handle(frame);
256+
}
257+
});
222258
FrameParser parser = new FrameParser(options);
223259
socket.exceptionHandler((exception) -> {
224260
LOGGER.error("The STOMP server caught a WebSocket error - closing connection", exception);
@@ -232,7 +268,21 @@ public Handler<ServerWebSocket> webSocketHandler() {
232268
connection.close();
233269
}
234270
)
235-
.handler(frame -> stomp.handle(new ServerFrameImpl(frame, connection)));
271+
.handler(frame -> {
272+
if (frame.getCommand() == Command.CONNECT) {
273+
if (firstFrame.compareAndSet(false, true)) {
274+
stomp.handle(new ServerFrameImpl(frame, connection));
275+
} else {
276+
connection.write(Frames.createErrorFrame("Already connected", Collections.emptyMap(), ""));
277+
connection.close();
278+
}
279+
} else if (connected.get()) {
280+
stomp.handle(new ServerFrameImpl(frame, connection));
281+
} else {
282+
connection.write(Frames.createErrorFrame("Not connected", Collections.emptyMap(), ""));
283+
connection.close();
284+
}
285+
});
236286
socket.handler(parser);
237287
};
238288
}

Diff for: src/test/java/io/vertx/ext/stomp/impl/SecuredServerConnectionTest.java

+79-4
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@
1616

1717
package io.vertx.ext.stomp.impl;
1818

19+
import io.vertx.core.Future;
20+
import io.vertx.core.Handler;
1921
import io.vertx.core.Vertx;
2022
import io.vertx.core.buffer.Buffer;
23+
import io.vertx.core.http.HttpClient;
24+
import io.vertx.core.http.HttpServer;
25+
import io.vertx.core.http.HttpServerOptions;
26+
import io.vertx.core.net.NetClient;
2127
import io.vertx.core.net.NetSocket;
2228
import io.vertx.ext.auth.User;
2329
import io.vertx.ext.auth.authentication.AuthenticationProvider;
@@ -38,15 +44,21 @@
3844
import org.junit.Test;
3945
import org.junit.runner.RunWith;
4046

47+
import java.util.Arrays;
48+
4149
/**
4250
* Tests STOMP server with security.
4351
*
4452
* @author <a href="http://escoffier.me">Clement Escoffier</a>
4553
*/
4654
@RunWith(VertxUnitRunner.class)
4755
public class SecuredServerConnectionTest {
56+
4857
private Vertx vertx;
4958
private StompServer server;
59+
private HttpServer wsServer;
60+
private HttpClient wsClient;
61+
private StompClient client;
5062

5163
@Rule
5264
public RunTestOnContext rule = new RunTestOnContext();
@@ -55,9 +67,17 @@ public class SecuredServerConnectionTest {
5567
public void setUp(TestContext context) {
5668
vertx = rule.vertx();
5769
AuthenticationProvider provider = PropertyFileAuthentication.create(vertx, "test-auth.properties");
58-
server = StompServer.create(vertx, new StompServerOptions().setSecured(true))
59-
.handler(StompServerHandler.create(vertx).authProvider(provider));
60-
server.listen().onComplete(context.asyncAssertSuccess());
70+
server = StompServer.create(vertx, new StompServerOptions()
71+
.setSecured(true)
72+
.setWebsocketBridge(true)
73+
.setWebsocketPath("/stomp"))
74+
.handler(StompServerHandler.create(vertx).authProvider(provider));
75+
server.listen(StompServerOptions.DEFAULT_STOMP_PORT).onComplete(context.asyncAssertSuccess());
76+
wsServer = vertx.createHttpServer(new HttpServerOptions().setWebSocketSubProtocols(Arrays.asList("v10.stomp", "v11.stomp")))
77+
.webSocketHandler(server.webSocketHandler());
78+
wsServer.listen(8080).onComplete(context.asyncAssertSuccess());
79+
wsClient = vertx.createHttpClient();
80+
client = StompClient.create(vertx, new StompClientOptions().setLogin("admin").setPasscode("admin"));
6181
}
6282

6383
@After
@@ -162,11 +182,66 @@ public void testClientConnectRejection(TestContext context) {
162182
}
163183

164184
void validate(TestContext context, Buffer buffer) {
165-
context.assertTrue(buffer.toString().contains("CONNECTED"));
185+
context.assertTrue(buffer.toString().contains("CONNECTED"), "Was expected <" + buffer.toString() + "> to contain 'CONNECTED'");
166186
context.assertTrue(buffer.toString().contains("version:1.2"));
167187

168188
User user = server.stompHandler().getUserBySession(extractSession(buffer.toString()));
169189
context.assertNotNull(user);
170190
}
171191

192+
@Test
193+
public void testTCPClientMustBeConnected(TestContext context) {
194+
Async async = context.async();
195+
NetClient client = vertx.createNetClient();
196+
testClientMustBeConnected(context, v -> {
197+
client.connect(server.actualPort(), "0.0.0.0").onComplete(context.asyncAssertSuccess(so -> {
198+
Buffer received = Buffer.buffer();
199+
so.handler(received::appendBuffer);
200+
so.write(
201+
"SEND\n" +
202+
"destination:/test\n" +
203+
"\n" +
204+
"hello" +
205+
FrameParser.NULL);
206+
so.endHandler(v2 -> {
207+
context.assertTrue(received.toString().startsWith("ERROR\n"));
208+
async.complete();
209+
});
210+
}));
211+
});
212+
}
213+
214+
@Test
215+
public void testWebSocketClientMustBeConnected(TestContext context) {
216+
Async async = context.async();
217+
testClientMustBeConnected(context, v -> {
218+
wsClient.webSocket(8080, "localhost", "/stomp").onComplete(context.asyncAssertSuccess(ws -> {
219+
Buffer received = Buffer.buffer();
220+
ws.binaryMessageHandler(received::appendBuffer);
221+
ws.writeBinaryMessage(
222+
Buffer.buffer("SEND\n" +
223+
"destination:/test\n" +
224+
"\n" +
225+
"hello" +
226+
FrameParser.NULL));
227+
ws.endHandler(v2 -> {
228+
context.assertTrue(received.toString().startsWith("ERROR\n"));
229+
async.complete();
230+
});
231+
}));
232+
});
233+
}
234+
235+
private void testClientMustBeConnected(TestContext context, Handler<Void> cont) {
236+
client
237+
.connect(server.actualPort(), "localhost")
238+
.onComplete(context.asyncAssertSuccess(conn -> {
239+
Future<String> fut = conn.subscribe("/test", frame -> {
240+
context.fail("Should not receive a messsage");
241+
});
242+
fut.onComplete(context.asyncAssertSuccess(v2 -> {
243+
cont.handle(null);
244+
}));
245+
}));
246+
}
172247
}

0 commit comments

Comments
 (0)