From a37de4556ba2663a0295f58c013bceba3e5559c7 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 21:44:10 +0200 Subject: [PATCH] fix(gameclients): bound login session inputs --- .../habbohotel/gameclients/GameClient.java | 6 +++++ .../incoming/handshake/SecureLoginEvent.java | 14 +++++----- .../handshake/SecureLoginInputGuard.java | 20 ++++++++++++++ .../GameClientManagerContractTest.java | 8 ++++++ .../handshake/SecureLoginInputGuardTest.java | 27 +++++++++++++++++++ 5 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/handshake/SecureLoginInputGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java index 87d5fb3d..a3af0226 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java @@ -14,6 +14,7 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; public class GameClient { @@ -24,6 +25,7 @@ public class GameClient { private final LatencyTracker latencyTracker; private Habbo habbo; + private final AtomicBoolean disposed = new AtomicBoolean(false); private boolean handshakeFinished; private String machineId = ""; private String ssoTicket = ""; @@ -153,6 +155,10 @@ public class GameClient { } public void dispose(boolean allowSessionResume) { + if (!this.disposed.compareAndSet(false, true)) { + return; + } + try { this.channel.close(); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java index f9704bec..c65abab3 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java @@ -72,7 +72,13 @@ public class SecureLoginEvent extends MessageHandler { return; } - String sso = this.packet.readString().replace(" ", ""); + String sso = SecureLoginInputGuard.normalizeSsoTicket(this.packet.readString()); + + if (!SecureLoginInputGuard.isValidSsoTicket(sso)) { + Emulator.getGameServer().getGameClientManager().disposeClient(this.client); + LOGGER.debug("Client is trying to connect with an invalid SSO ticket! Closed connection..."); + return; + } if (Emulator.getPluginManager().fireEvent(new SSOAuthenticationEvent(sso)).isCancelled()) { Emulator.getGameServer().getGameClientManager().disposeClient(this.client); @@ -80,12 +86,6 @@ public class SecureLoginEvent extends MessageHandler { return; } - if (sso.isEmpty()) { - Emulator.getGameServer().getGameClientManager().disposeClient(this.client); - LOGGER.debug("Client is trying to connect without SSO ticket! Closed connection..."); - return; - } - if (this.client.getHabbo() == null) { // Store SSO ticket on client for grace period tracking this.client.setSsoTicket(sso); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginInputGuard.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginInputGuard.java new file mode 100644 index 00000000..b9a5a9b0 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginInputGuard.java @@ -0,0 +1,20 @@ +package com.eu.habbo.messages.incoming.handshake; + +final class SecureLoginInputGuard { + static final int MAX_SSO_TICKET_LENGTH = 512; + + private SecureLoginInputGuard() { + } + + static String normalizeSsoTicket(String ticket) { + if (ticket == null) { + return ""; + } + + return ticket.replace(" ", ""); + } + + static boolean isValidSsoTicket(String ticket) { + return ticket != null && !ticket.isEmpty() && ticket.length() <= MAX_SSO_TICKET_LENGTH; + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/gameclients/GameClientManagerContractTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/gameclients/GameClientManagerContractTest.java index 4f35e668..27bc393f 100644 --- a/Emulator/src/test/java/com/eu/habbo/habbohotel/gameclients/GameClientManagerContractTest.java +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/gameclients/GameClientManagerContractTest.java @@ -3,6 +3,7 @@ package com.eu.habbo.habbohotel.gameclients; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; class GameClientManagerContractTest { @@ -19,4 +20,11 @@ class GameClientManagerContractTest { assertDoesNotThrow(() -> manager.disposeClient(null)); assertDoesNotThrow(() -> manager.forceDisposeClient(null)); } + + @Test + void gameClientDisposeIsExplicitlyIdempotent() throws Exception { + assertTrue(java.util.concurrent.atomic.AtomicBoolean.class.isAssignableFrom( + GameClient.class.getDeclaredField("disposed").getType() + )); + } } diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/handshake/SecureLoginInputGuardTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/handshake/SecureLoginInputGuardTest.java new file mode 100644 index 00000000..28ec4504 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/handshake/SecureLoginInputGuardTest.java @@ -0,0 +1,27 @@ +package com.eu.habbo.messages.incoming.handshake; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SecureLoginInputGuardTest { + + @Test + void normalizesNullAndSpacesBeforeAuthentication() { + assertEquals("", SecureLoginInputGuard.normalizeSsoTicket(null)); + assertEquals("abc123", SecureLoginInputGuard.normalizeSsoTicket(" abc 123 ")); + } + + @Test + void rejectsMissingOrOversizedTickets() { + assertFalse(SecureLoginInputGuard.isValidSsoTicket("")); + assertFalse(SecureLoginInputGuard.isValidSsoTicket("x".repeat(SecureLoginInputGuard.MAX_SSO_TICKET_LENGTH + 1))); + } + + @Test + void acceptsTicketWithinBound() { + assertTrue(SecureLoginInputGuard.isValidSsoTicket("x".repeat(SecureLoginInputGuard.MAX_SSO_TICKET_LENGTH))); + } +}