From 37ce71ad1ed13cbdd335f85b4b514c731646f642 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 15 Jun 2026 22:19:29 +0200 Subject: [PATCH] fix(auth): bound session token inputs --- .../incoming/handshake/SecureLoginEvent.java | 5 ++- .../gameserver/auth/AccessTokenService.java | 3 +- .../gameserver/auth/RememberJwtService.java | 3 +- .../SecureLoginGuardContractTest.java | 28 +++++++++++++ .../auth/AuthTokenGuardContractTest.java | 41 +++++++++++++++++++ 5 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/handshake/SecureLoginGuardContractTest.java create mode 100644 Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/AuthTokenGuardContractTest.java 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..945e7b73 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 @@ -50,6 +50,7 @@ import java.util.Date; @NoAuthMessage public class SecureLoginEvent extends MessageHandler { private static final Logger LOGGER = LoggerFactory.getLogger(SecureLoginEvent.class); + private static final int MAX_SSO_TICKET_LENGTH = 128; @Override public int getRatelimit() { @@ -80,9 +81,9 @@ public class SecureLoginEvent extends MessageHandler { return; } - if (sso.isEmpty()) { + if (sso.isEmpty() || sso.length() > MAX_SSO_TICKET_LENGTH) { Emulator.getGameServer().getGameClientManager().disposeClient(this.client); - LOGGER.debug("Client is trying to connect without SSO ticket! Closed connection..."); + LOGGER.debug("Client is trying to connect with missing or invalid SSO ticket! Closed connection..."); return; } diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AccessTokenService.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AccessTokenService.java index bb15c8b1..36135e08 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AccessTokenService.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/AccessTokenService.java @@ -21,6 +21,7 @@ public final class AccessTokenService { private static final SecureRandom RNG = new SecureRandom(); private static final Base64.Encoder URL_ENC = Base64.getUrlEncoder().withoutPadding(); private static final Base64.Decoder URL_DEC = Base64.getUrlDecoder(); + private static final int MAX_TOKEN_CHARS = 2048; private static volatile String cachedSecret = null; @@ -63,7 +64,7 @@ public final class AccessTokenService { } public static int verify(String token) { - if (token == null || token.isEmpty()) return 0; + if (token == null || token.isEmpty() || token.length() > MAX_TOKEN_CHARS) return 0; String[] parts = token.split("\\."); if (parts.length != 3) return 0; diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/RememberJwtService.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/RememberJwtService.java index 93763902..3a1747d4 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/RememberJwtService.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/RememberJwtService.java @@ -24,6 +24,7 @@ public final class RememberJwtService { private static final SecureRandom RNG = new SecureRandom(); private static final Base64.Encoder URL_ENC = Base64.getUrlEncoder().withoutPadding(); private static final Base64.Decoder URL_DEC = Base64.getUrlDecoder(); + private static final int MAX_TOKEN_CHARS = 2048; private static volatile String cachedSecret = null; @@ -238,7 +239,7 @@ public final class RememberJwtService { } private static ParsedJwt verifyAndParse(String jwt) throws Exception { - if (jwt == null || jwt.isEmpty()) throw new IllegalArgumentException("empty"); + if (jwt == null || jwt.isEmpty() || jwt.length() > MAX_TOKEN_CHARS) throw new IllegalArgumentException("empty"); String[] parts = jwt.split("\\."); if (parts.length != 3) throw new IllegalArgumentException("not 3 segments"); diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/handshake/SecureLoginGuardContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/handshake/SecureLoginGuardContractTest.java new file mode 100644 index 00000000..149a5f86 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/handshake/SecureLoginGuardContractTest.java @@ -0,0 +1,28 @@ +package com.eu.habbo.messages.incoming.handshake; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SecureLoginGuardContractTest { + + private static String source() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java")); + } + + @Test + void websocketSsoTicketIsLengthBoundedBeforeDatabaseLookup() throws Exception { + String source = source(); + + int maxConstant = source.indexOf("MAX_SSO_TICKET_LENGTH = 128"); + int guard = source.indexOf("sso.isEmpty() || sso.length() > MAX_SSO_TICKET_LENGTH"); + int lookup = source.indexOf("SELECT id FROM users WHERE auth_ticket = ?"); + + assertTrue(maxConstant > -1, "Secure login should define the same SSO length cap used by HTTP auth"); + assertTrue(guard > -1, "Secure login must reject missing or oversized SSO tickets"); + assertTrue(guard < lookup, "SSO length must be validated before database lookup"); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/AuthTokenGuardContractTest.java b/Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/AuthTokenGuardContractTest.java new file mode 100644 index 00000000..1aa5a0ba --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/AuthTokenGuardContractTest.java @@ -0,0 +1,41 @@ +package com.eu.habbo.networking.gameserver.auth; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AuthTokenGuardContractTest { + + @Test + void accessTokenRejectsOversizedTokensBeforeSplitAndDecode() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/networking/gameserver/auth/AccessTokenService.java")); + + int maxConstant = source.indexOf("MAX_TOKEN_CHARS = 2048"); + int lengthGuard = source.indexOf("token.length() > MAX_TOKEN_CHARS"); + int split = source.indexOf("token.split"); + int decode = source.indexOf("URL_DEC.decode"); + + assertTrue(maxConstant > -1, "Access tokens should have a bounded serialized size"); + assertTrue(lengthGuard > -1, "Access token verification must reject oversized tokens"); + assertTrue(lengthGuard < split, "Access token length guard must run before split"); + assertTrue(lengthGuard < decode, "Access token length guard must run before Base64 decode"); + } + + @Test + void rememberTokenRejectsOversizedTokensBeforeSplitAndDecode() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/networking/gameserver/auth/RememberJwtService.java")); + + int maxConstant = source.indexOf("MAX_TOKEN_CHARS = 2048"); + int lengthGuard = source.indexOf("jwt.length() > MAX_TOKEN_CHARS"); + int split = source.indexOf("jwt.split"); + int decode = source.indexOf("URL_DEC.decode"); + + assertTrue(maxConstant > -1, "Remember tokens should have a bounded serialized size"); + assertTrue(lengthGuard > -1, "Remember token verification must reject oversized tokens"); + assertTrue(lengthGuard < split, "Remember token length guard must run before split"); + assertTrue(lengthGuard < decode, "Remember token length guard must run before Base64 decode"); + } +}