From 37ce71ad1ed13cbdd335f85b4b514c731646f642 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 15 Jun 2026 22:19:29 +0200 Subject: [PATCH 01/18] 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"); + } +} From 1598297a2a96ce6ba54d85e567ef4e23d308b705 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 15 Jun 2026 22:39:29 +0200 Subject: [PATCH 02/18] fix(auth): bound secure asset file reads --- .../src/main/java/com/eu/habbo/Emulator.java | 2 + .../auth/NitroSecureAssetHandler.java | 13 +++++ .../NitroSecureAssetHandlerContractTest.java | 48 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandlerContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/Emulator.java b/Emulator/src/main/java/com/eu/habbo/Emulator.java index 52c7f8b1..a9f70bd7 100644 --- a/Emulator/src/main/java/com/eu/habbo/Emulator.java +++ b/Emulator/src/main/java/com/eu/habbo/Emulator.java @@ -166,6 +166,8 @@ public final class Emulator { Emulator.config.register("rcon.rate_limit.timeout_ms", "0"); Emulator.config.register("rcon.execute_command.denied_permissions", "cmd_shutdown;cmd_give_rank"); Emulator.config.register("rcon.execute_command.allowed_permissions", ""); + Emulator.config.register("nitro.secure.config.max_file_bytes", "2097152"); + Emulator.config.register("nitro.secure.gamedata.max_file_bytes", "16777216"); registerEarningsSettings(); String hotelTimezoneId = Emulator.getConfig().getValue("hotel.timezone", java.time.ZoneId.systemDefault().getId()); System.out.println(startupCard(hotelTimezoneId)); diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java index 851cf90b..645408da 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java @@ -35,6 +35,8 @@ public class NitroSecureAssetHandler extends ChannelInboundHandlerAdapter { private static final String BOOTSTRAP_PATH = "/nitro-sec/bootstrap"; private static final String FILE_PATH = "/nitro-sec/file"; private static final int MAX_BOOTSTRAP_BODY_BYTES = 4096; + private static final int DEFAULT_MAX_CONFIG_BYTES = 2 * 1024 * 1024; + private static final int DEFAULT_MAX_GAMEDATA_BYTES = 16 * 1024 * 1024; private static final SecureRandom RNG = new SecureRandom(); private static final KeyPair SERVER_KEYPAIR = createServerKeyPair(); private static final String SERVER_KEY_FINGERPRINT = fingerprint(SERVER_KEYPAIR.getPublic().getEncoded()); @@ -146,6 +148,9 @@ public class NitroSecureAssetHandler extends ChannelInboundHandlerAdapter { if (!target.startsWith(root)) throw new IllegalArgumentException("Invalid file."); if (!Files.isRegularFile(target)) throw new IOException("Not found"); + long size = Files.size(target); + int maxBytes = maxAssetBytes(kind); + if (size > maxBytes) throw new IllegalArgumentException("File too large."); String cacheKey = kind + ":" + target; long modified = Files.getLastModifiedTime(target).toMillis(); @@ -158,6 +163,14 @@ public class NitroSecureAssetHandler extends ChannelInboundHandlerAdapter { return bytes; } + static int maxAssetBytes(String kind) { + boolean config = "config".equals(kind); + String key = config ? "nitro.secure.config.max_file_bytes" : "nitro.secure.gamedata.max_file_bytes"; + int fallback = config ? DEFAULT_MAX_CONFIG_BYTES : DEFAULT_MAX_GAMEDATA_BYTES; + int configured = Emulator.getConfig().getInt(key, fallback); + return configured > 0 ? configured : fallback; + } + private static String normalizeFile(String file) { if (file == null) throw new IllegalArgumentException("Missing file."); String value = URLDecoder.decode(file, StandardCharsets.UTF_8).replace('\\', '/'); diff --git a/Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandlerContractTest.java b/Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandlerContractTest.java new file mode 100644 index 00000000..5bc8465e --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandlerContractTest.java @@ -0,0 +1,48 @@ +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 NitroSecureAssetHandlerContractTest { + private static String handlerSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java")); + } + + private static String emulatorSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/Emulator.java")); + } + + @Test + void secureAssetFilesAreSizeCheckedBeforeReadAndCache() throws Exception { + String handler = handlerSource(); + String emulator = emulatorSource(); + + int size = handler.indexOf("long size = Files.size(target)"); + int maxBytes = handler.indexOf("int maxBytes = maxAssetBytes(kind)", size); + int oversizedGuard = handler.indexOf("size > maxBytes", maxBytes); + int cacheLookup = handler.indexOf("CACHE.get(cacheKey)", oversizedGuard); + int readAllBytes = handler.indexOf("Files.readAllBytes(target)", oversizedGuard); + + assertTrue(handler.contains("DEFAULT_MAX_CONFIG_BYTES = 2 * 1024 * 1024"), + "Secure config assets should have a conservative default file cap"); + assertTrue(handler.contains("DEFAULT_MAX_GAMEDATA_BYTES = 16 * 1024 * 1024"), + "Secure gamedata assets should have a bounded default file cap"); + assertTrue(handler.contains("nitro.secure.config.max_file_bytes"), + "Secure config max file size should be configurable"); + assertTrue(handler.contains("nitro.secure.gamedata.max_file_bytes"), + "Secure gamedata max file size should be configurable"); + assertTrue(size > -1, "Secure assets must inspect file size before loading bytes"); + assertTrue(maxBytes > size, "Secure assets must resolve the configured cap before loading bytes"); + assertTrue(oversizedGuard > maxBytes, "Secure assets must reject oversized files"); + assertTrue(oversizedGuard < cacheLookup, "Oversized secure assets must not be served from cache"); + assertTrue(oversizedGuard < readAllBytes, "Oversized secure assets must be rejected before readAllBytes"); + assertTrue(emulator.contains("register(\"nitro.secure.config.max_file_bytes\", \"2097152\")"), + "Secure config max file size default must be registered before startup"); + assertTrue(emulator.contains("register(\"nitro.secure.gamedata.max_file_bytes\", \"16777216\")"), + "Secure gamedata max file size default must be registered before startup"); + } +} From b600ac499cedf02a26a836c7d79dcea3cd2791f5 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 20:04:03 +0200 Subject: [PATCH 03/18] fix(trading): bound offered item batches --- .../eu/habbo/habbohotel/rooms/RoomTrade.java | 6 ++- .../trading/TradeOfferMultipleItemsEvent.java | 9 +++- .../trading/TradeOfferGuardContractTest.java | 50 +++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/trading/TradeOfferGuardContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTrade.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTrade.java index 24ca36ea..13a4deba 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTrade.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/rooms/RoomTrade.java @@ -23,6 +23,7 @@ public class RoomTrade { //Configuration. Loaded from database & updated accordingly. public static boolean TRADING_ENABLED = true; public static boolean TRADING_REQUIRES_PERK = true; + public static final int MAX_OFFERED_ITEMS = 100; private final List users; private final Room room; @@ -58,7 +59,7 @@ public class RoomTrade { public synchronized void offerItem(Habbo habbo, HabboItem item) { RoomTradeUser user = this.getRoomTradeUserForHabbo(habbo); - if (user == null || item == null || user.getItems().contains(item)) + if (user == null || item == null || user.getItems().contains(item) || user.getItems().size() >= MAX_OFFERED_ITEMS) return; habbo.getInventory().getItemsComponent().removeHabboItem(item); @@ -75,6 +76,9 @@ public class RoomTrade { return; for (HabboItem item : items) { + if (user.getItems().size() >= MAX_OFFERED_ITEMS) + break; + if (!user.getItems().contains(item)) { habbo.getInventory().getItemsComponent().removeHabboItem(item); user.getItems().add(item); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/trading/TradeOfferMultipleItemsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/trading/TradeOfferMultipleItemsEvent.java index 231a70c1..29c7a227 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/trading/TradeOfferMultipleItemsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/trading/TradeOfferMultipleItemsEvent.java @@ -19,8 +19,15 @@ public class TradeOfferMultipleItemsEvent extends MessageHandler { THashSet items = new THashSet<>(); int count = this.packet.readInt(); + if (count <= 0 || count > RoomTrade.MAX_OFFERED_ITEMS) + return; + for (int i = 0; i < count; i++) { - HabboItem item = this.client.getHabbo().getInventory().getItemsComponent().getHabboItem(this.packet.readInt()); + int itemId = this.packet.readInt(); + if (itemId <= 0) + continue; + + HabboItem item = this.client.getHabbo().getInventory().getItemsComponent().getHabboItem(itemId); if (item != null && item.getBaseItem().allowTrade()) { items.add(item); } diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/trading/TradeOfferGuardContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/trading/TradeOfferGuardContractTest.java new file mode 100644 index 00000000..b8917cd7 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/trading/TradeOfferGuardContractTest.java @@ -0,0 +1,50 @@ +package com.eu.habbo.messages.incoming.trading; + +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 TradeOfferGuardContractTest { + private static String incomingSource(String name) throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/trading/" + name + ".java")); + } + + private static String roomTradeSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/rooms/RoomTrade.java")); + } + + @Test + void multipleTradeOfferPacketBoundsClientSuppliedCountBeforeInventoryLookups() throws Exception { + String source = incomingSource("TradeOfferMultipleItemsEvent"); + + int count = source.indexOf("int count = this.packet.readInt()"); + int guard = source.indexOf("count <= 0 || count > RoomTrade.MAX_OFFERED_ITEMS", count); + int loop = source.indexOf("for (int i = 0; i < count; i++)", count); + int lookup = source.indexOf("getHabboItem(itemId)", loop); + + assertTrue(count > -1, "TradeOfferMultipleItemsEvent must read the client supplied count"); + assertTrue(guard > count, "TradeOfferMultipleItemsEvent must validate the count after reading it"); + assertTrue(guard < loop, "TradeOfferMultipleItemsEvent must validate the count before looping"); + assertTrue(loop < lookup, "TradeOfferMultipleItemsEvent should only resolve inventory items inside the bounded loop"); + assertTrue(source.contains("itemId <= 0"), + "TradeOfferMultipleItemsEvent must skip invalid item ids before inventory lookup"); + } + + @Test + void roomTradeEnforcesServerSideOfferedItemCapBeforeInventoryMutation() throws Exception { + String source = roomTradeSource(); + + int constant = source.indexOf("MAX_OFFERED_ITEMS = 100"); + int singleGuard = source.indexOf("user.getItems().size() >= MAX_OFFERED_ITEMS"); + int multipleGuard = source.indexOf("user.getItems().size() >= MAX_OFFERED_ITEMS", singleGuard + 1); + int remove = source.indexOf("removeHabboItem(item)", multipleGuard); + + assertTrue(constant > -1, "RoomTrade must define a server-side offered item cap"); + assertTrue(singleGuard > constant, "RoomTrade.offerItem must enforce the item cap"); + assertTrue(multipleGuard > singleGuard, "RoomTrade.offerMultipleItems must enforce the item cap"); + assertTrue(multipleGuard < remove, "RoomTrade must enforce the cap before mutating inventory"); + } +} From a433e5539d300aee479689c55b096d8b93db34a0 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 20:06:32 +0200 Subject: [PATCH 04/18] fix(rcon): bound inbound payload handling --- .../src/main/java/com/eu/habbo/Emulator.java | 1 + .../rconserver/RCONServerHandler.java | 50 ++++++++++++++++--- .../RCONServerHandlerContractTest.java | 36 +++++++++++++ 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/Emulator.java b/Emulator/src/main/java/com/eu/habbo/Emulator.java index 52c7f8b1..ea27325a 100644 --- a/Emulator/src/main/java/com/eu/habbo/Emulator.java +++ b/Emulator/src/main/java/com/eu/habbo/Emulator.java @@ -166,6 +166,7 @@ public final class Emulator { Emulator.config.register("rcon.rate_limit.timeout_ms", "0"); Emulator.config.register("rcon.execute_command.denied_permissions", "cmd_shutdown;cmd_give_rank"); Emulator.config.register("rcon.execute_command.allowed_permissions", ""); + Emulator.config.register("rcon.max_payload_bytes", "65536"); registerEarningsSettings(); String hotelTimezoneId = Emulator.getConfig().getValue("hotel.timezone", java.time.ZoneId.systemDefault().getId()); System.out.println(startupCard(hotelTimezoneId)); diff --git a/Emulator/src/main/java/com/eu/habbo/networking/rconserver/RCONServerHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/rconserver/RCONServerHandler.java index 014ba0ec..7c81c050 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/rconserver/RCONServerHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/rconserver/RCONServerHandler.java @@ -12,6 +12,9 @@ import io.netty.channel.ChannelInboundHandlerAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.InetSocketAddress; +import java.net.SocketAddress; + public class RCONServerHandler extends ChannelInboundHandlerAdapter { private static final Logger LOGGER = LoggerFactory.getLogger(RCONServerHandler.class); @@ -19,20 +22,21 @@ public class RCONServerHandler extends ChannelInboundHandlerAdapter { // Gson is thread-safe and immutable once built — share one instance instead // of allocating a parser per RCON request. private static final Gson GSON = new Gson(); + private static final int DEFAULT_MAX_PAYLOAD_BYTES = 64 * 1024; @Override public void channelRegistered(ChannelHandlerContext ctx) throws Exception { - String adress = ctx.channel().remoteAddress().toString().split(":")[0].replace("/", ""); + String address = remoteAddress(ctx); for (String s : Emulator.getRconServer().allowedAdresses) { - if (s.equalsIgnoreCase(adress)) { + if (s.equalsIgnoreCase(address)) { return; } } ctx.channel().close(); - LOGGER.warn("RCON Remote connection closed: {}. IP not allowed!", adress); + LOGGER.warn("RCON Remote connection closed: {}. IP not allowed!", address); } @Override @@ -43,7 +47,15 @@ public class RCONServerHandler extends ChannelInboundHandlerAdapter { } try { - byte[] d = new byte[data.readableBytes()]; + int readableBytes = data.readableBytes(); + int maxPayloadBytes = maxPayloadBytes(); + if (readableBytes > maxPayloadBytes) { + writeAndClose(ctx, "PAYLOAD_TOO_LARGE"); + LOGGER.warn("Rejected oversized RCON payload: {} bytes (max {})", readableBytes, maxPayloadBytes); + return; + } + + byte[] d = new byte[readableBytes]; data.getBytes(0, d); String message = new String(d, java.nio.charset.StandardCharsets.UTF_8); Gson gson = GSON; @@ -60,12 +72,34 @@ public class RCONServerHandler extends ChannelInboundHandlerAdapter { e.printStackTrace(); } - ChannelFuture f = ctx.channel().write(Unpooled.copiedBuffer(response.getBytes(java.nio.charset.StandardCharsets.UTF_8)), ctx.channel().voidPromise()); - ctx.channel().flush(); - ctx.flush(); - f.channel().close(); + writeAndClose(ctx, response); } finally { data.release(); } } + + static int maxPayloadBytes() { + if (Emulator.getConfig() == null) { + return DEFAULT_MAX_PAYLOAD_BYTES; + } + + int configured = Emulator.getConfig().getInt("rcon.max_payload_bytes", DEFAULT_MAX_PAYLOAD_BYTES); + return configured > 0 ? configured : DEFAULT_MAX_PAYLOAD_BYTES; + } + + static String remoteAddress(ChannelHandlerContext ctx) { + SocketAddress socketAddress = ctx.channel().remoteAddress(); + if (socketAddress instanceof InetSocketAddress inetSocketAddress && inetSocketAddress.getAddress() != null) { + return inetSocketAddress.getAddress().getHostAddress(); + } + + return socketAddress == null ? "" : socketAddress.toString().replace("/", ""); + } + + private static void writeAndClose(ChannelHandlerContext ctx, String response) { + ChannelFuture f = ctx.channel().write(Unpooled.copiedBuffer(response.getBytes(java.nio.charset.StandardCharsets.UTF_8)), ctx.channel().voidPromise()); + ctx.channel().flush(); + ctx.flush(); + f.channel().close(); + } } diff --git a/Emulator/src/test/java/com/eu/habbo/networking/rconserver/RCONServerHandlerContractTest.java b/Emulator/src/test/java/com/eu/habbo/networking/rconserver/RCONServerHandlerContractTest.java index 893d556b..54e6586a 100644 --- a/Emulator/src/test/java/com/eu/habbo/networking/rconserver/RCONServerHandlerContractTest.java +++ b/Emulator/src/test/java/com/eu/habbo/networking/rconserver/RCONServerHandlerContractTest.java @@ -50,6 +50,30 @@ class RCONServerHandlerContractTest { assertTrue(registerIndex < serverIndex, "RCON rate limit defaults must be registered before RCONServer is constructed"); } + @Test + void rconPayloadSizeIsBoundedBeforeBufferCopy() throws Exception { + String handler = handlerSource(); + String emulator = emulatorSource(); + + int readableBytes = handler.indexOf("int readableBytes = data.readableBytes()"); + int maxPayload = handler.indexOf("int maxPayloadBytes = maxPayloadBytes()", readableBytes); + int oversizedGuard = handler.indexOf("readableBytes > maxPayloadBytes", maxPayload); + int byteArray = handler.indexOf("new byte[readableBytes]", readableBytes); + + assertTrue(handler.contains("DEFAULT_MAX_PAYLOAD_BYTES = 64 * 1024"), + "RCON handler should have a conservative default payload cap"); + assertTrue(handler.contains("rcon.max_payload_bytes"), + "RCON max payload should be configurable"); + assertTrue(readableBytes > -1, "RCON handler must read ByteBuf size before allocation"); + assertTrue(maxPayload > readableBytes, "RCON handler must resolve max payload before allocation"); + assertTrue(oversizedGuard > maxPayload, "RCON handler must reject oversized payloads"); + assertTrue(oversizedGuard < byteArray, "Oversized RCON payloads must be rejected before byte array allocation"); + assertTrue(handler.contains("PAYLOAD_TOO_LARGE"), + "RCON callers need a deterministic response for oversized payloads"); + assertTrue(emulator.contains("register(\"rcon.max_payload_bytes\", \"65536\")"), + "RCON max payload default must be registered before startup"); + } + @Test void inboundByteBufIsReleasedFromFinallyBlock() throws Exception { String source = handlerSource(); @@ -59,4 +83,16 @@ class RCONServerHandlerContractTest { assertTrue(finallyIndex >= 0, "RCON channelRead must release inbound ByteBufs from a finally block"); assertTrue(releaseIndex > finallyIndex, "RCON channelRead must release the inbound ByteBuf after finally starts"); } + + @Test + void rconWhitelistUsesSocketAddressInsteadOfStringSplitting() throws Exception { + String source = handlerSource(); + + assertTrue(source.contains("InetSocketAddress"), + "RCON whitelist should resolve socket addresses instead of parsing remoteAddress.toString()"); + assertTrue(source.contains("getHostAddress()"), + "RCON whitelist should compare the resolved host address"); + assertTrue(!source.contains(".toString().split(\":\")"), + "RCON whitelist must not split host:port strings because that breaks IPv6 addresses"); + } } From 547c5ef157769e915189db99944a392e29c6b64e Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 20:08:42 +0200 Subject: [PATCH 05/18] fix(auth): bound secure api payloads --- .../src/main/java/com/eu/habbo/Emulator.java | 1 + .../auth/NitroSecureApiHandler.java | 20 ++++++++- .../NitroSecureApiHandlerContractTest.java | 44 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandlerContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/Emulator.java b/Emulator/src/main/java/com/eu/habbo/Emulator.java index 52c7f8b1..d776d283 100644 --- a/Emulator/src/main/java/com/eu/habbo/Emulator.java +++ b/Emulator/src/main/java/com/eu/habbo/Emulator.java @@ -166,6 +166,7 @@ public final class Emulator { Emulator.config.register("rcon.rate_limit.timeout_ms", "0"); Emulator.config.register("rcon.execute_command.denied_permissions", "cmd_shutdown;cmd_give_rank"); Emulator.config.register("rcon.execute_command.allowed_permissions", ""); + Emulator.config.register("nitro.secure.api.max_payload_bytes", "65536"); registerEarningsSettings(); String hotelTimezoneId = Emulator.getConfig().getValue("hotel.timezone", java.time.ZoneId.systemDefault().getId()); System.out.println(startupCard(hotelTimezoneId)); diff --git a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java index 45528857..afe09e5c 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java @@ -24,7 +24,9 @@ import java.util.concurrent.ConcurrentHashMap; public class NitroSecureApiHandler extends ChannelDuplexHandler { private static final Logger LOGGER = LoggerFactory.getLogger(NitroSecureApiHandler.class); private static final String ENABLED_CONFIG = "nitro.secure.api.enabled"; + private static final String MAX_PAYLOAD_CONFIG = "nitro.secure.api.max_payload_bytes"; private static final String API_PREFIX = "/api/"; + private static final int DEFAULT_MAX_PAYLOAD_BYTES = 64 * 1024; private static final AttributeKey> SECURE_CONTEXTS = AttributeKey.valueOf("nitroSecureApiContexts"); private static final ConcurrentHashMap NONCE_CACHE = new ConcurrentHashMap<>(); @@ -81,7 +83,14 @@ public class NitroSecureApiHandler extends ChannelDuplexHandler { return; } - byte[] encrypted = new byte[req.content().readableBytes()]; + int readableBytes = req.content().readableBytes(); + int maxPayloadBytes = maxPayloadBytes(); + if (readableBytes > maxPayloadBytes) { + sendText(ctx, req, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, "Secure payload too large."); + return; + } + + byte[] encrypted = new byte[readableBytes]; req.content().getBytes(req.content().readerIndex(), encrypted); byte[] clear = NitroSecureAssetHandler.decrypt(sessionKey, NitroSecureAssetHandler.fromHex(new String(encrypted, StandardCharsets.UTF_8))); clear = unwrapEnvelope(clear, req, secureContext); @@ -173,6 +182,15 @@ public class NitroSecureApiHandler extends ChannelDuplexHandler { return com.eu.habbo.Emulator.getConfig().getBoolean(ENABLED_CONFIG, true); } + static int maxPayloadBytes() { + if (com.eu.habbo.Emulator.getConfig() == null) { + return DEFAULT_MAX_PAYLOAD_BYTES; + } + + int configured = com.eu.habbo.Emulator.getConfig().getInt(MAX_PAYLOAD_CONFIG, DEFAULT_MAX_PAYLOAD_BYTES); + return configured > 0 ? configured : DEFAULT_MAX_PAYLOAD_BYTES; + } + private static byte[] unwrapEnvelope(byte[] clear, FullHttpRequest req, SecureApiContext secureContext) { if (!requiresReplayEnvelope(req.method())) return clear; diff --git a/Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandlerContractTest.java b/Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandlerContractTest.java new file mode 100644 index 00000000..0e708197 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandlerContractTest.java @@ -0,0 +1,44 @@ +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 NitroSecureApiHandlerContractTest { + private static String handlerSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java")); + } + + private static String emulatorSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/Emulator.java")); + } + + @Test + void encryptedApiPayloadSizeIsBoundedBeforeCopyAndDecrypt() throws Exception { + String handler = handlerSource(); + String emulator = emulatorSource(); + + int readableBytes = handler.indexOf("int readableBytes = req.content().readableBytes()"); + int maxPayload = handler.indexOf("int maxPayloadBytes = maxPayloadBytes()", readableBytes); + int oversizedGuard = handler.indexOf("readableBytes > maxPayloadBytes", maxPayload); + int byteArray = handler.indexOf("new byte[readableBytes]", readableBytes); + int decrypt = handler.indexOf("NitroSecureAssetHandler.decrypt", byteArray); + + assertTrue(handler.contains("DEFAULT_MAX_PAYLOAD_BYTES = 64 * 1024"), + "Secure API handler should have a conservative default payload cap"); + assertTrue(handler.contains("nitro.secure.api.max_payload_bytes"), + "Secure API max payload should be configurable"); + assertTrue(readableBytes > -1, "Secure API handler must read content size before allocation"); + assertTrue(maxPayload > readableBytes, "Secure API handler must resolve max payload before allocation"); + assertTrue(oversizedGuard > maxPayload, "Secure API handler must reject oversized encrypted payloads"); + assertTrue(oversizedGuard < byteArray, "Oversized encrypted payloads must be rejected before byte array allocation"); + assertTrue(byteArray < decrypt, "Secure API payload must be bounded before decrypting"); + assertTrue(handler.contains("REQUEST_ENTITY_TOO_LARGE"), + "Secure API callers need a deterministic status for oversized encrypted payloads"); + assertTrue(emulator.contains("register(\"nitro.secure.api.max_payload_bytes\", \"65536\")"), + "Secure API max payload default must be registered before startup"); + } +} From 4cf0af79d1042ad60b44e745b11322ec24e301fa Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 20:16:33 +0200 Subject: [PATCH 06/18] fix(rooms): validate room settings inputs --- .../incoming/rooms/RoomSettingsSaveEvent.java | 91 +++++++++++++++---- .../RoomSettingsInputGuardContractTest.java | 60 ++++++++++++ 2 files changed, 134 insertions(+), 17 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/RoomSettingsInputGuardContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RoomSettingsSaveEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RoomSettingsSaveEvent.java index 8200b5f8..51fad249 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RoomSettingsSaveEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/RoomSettingsSaveEvent.java @@ -15,6 +15,15 @@ import java.util.Set; public class RoomSettingsSaveEvent extends MessageHandler { private static final Logger LOGGER = LoggerFactory.getLogger(RoomSettingsSaveEvent.class); + private static final int MAX_ROOM_PASSWORD_LENGTH = 64; + private static final int MAX_TAGS = 2; + private static final int MIN_USERS_MAX = 1; + private static final int MAX_USERS_MAX = 200; + private static final int MIN_THICKNESS = -2; + private static final int MAX_THICKNESS = 1; + private static final int MAX_OPTION_LEVEL = 2; + private static final int MIN_CHAT_DISTANCE = 1; + private static final int MAX_CHAT_DISTANCE = 99; @Override public void handle() throws Exception { @@ -47,19 +56,33 @@ public class RoomSettingsSaveEvent extends MessageHandler { return; } - RoomState state = RoomState.values()[this.packet.readInt() % RoomState.values().length]; + int stateId = this.packet.readInt(); + if (stateId < 0 || stateId >= RoomState.values().length) { + return; + } + RoomState state = RoomState.values()[stateId]; String password = this.packet.readString(); + if (password.length() > MAX_ROOM_PASSWORD_LENGTH) { + return; + } if (state == RoomState.PASSWORD && password.isEmpty() && (room.getPassword() == null || room.getPassword().isEmpty())) { this.client.sendResponse(new RoomEditSettingsErrorComposer(room.getId(), RoomEditSettingsErrorComposer.PASSWORD_REQUIRED, "")); return; } int usersMax = this.packet.readInt(); + if (usersMax < MIN_USERS_MAX || usersMax > MAX_USERS_MAX) { + return; + } + int categoryId = this.packet.readInt(); StringBuilder tags = new StringBuilder(); Set uniqueTags = new HashSet<>(); - int count = Math.min(this.packet.readInt(), 2); + int count = this.packet.readInt(); + if (count < 0 || count > MAX_TAGS) { + return; + } for (int i = 0; i < count; i++) { String tag = this.packet.readString(); @@ -113,22 +136,52 @@ public class RoomSettingsSaveEvent extends MessageHandler { } + int tradeMode = this.packet.readInt(); + boolean allowPets = this.packet.readBoolean(); + boolean allowPetsEat = this.packet.readBoolean(); + boolean allowWalkthrough = this.packet.readBoolean(); + boolean hideWall = this.packet.readBoolean(); + int wallSize = this.packet.readInt(); + int floorSize = this.packet.readInt(); + int muteOption = this.packet.readInt(); + int kickOption = this.packet.readInt(); + int banOption = this.packet.readInt(); + int chatMode = this.packet.readInt(); + int chatWeight = this.packet.readInt(); + int chatSpeed = this.packet.readInt(); + int chatDistance = this.packet.readInt(); + int chatProtection = this.packet.readInt(); + + if (!isInRange(tradeMode, 0, MAX_OPTION_LEVEL) + || !isInRange(wallSize, MIN_THICKNESS, MAX_THICKNESS) + || !isInRange(floorSize, MIN_THICKNESS, MAX_THICKNESS) + || !isInRange(muteOption, 0, MAX_OPTION_LEVEL) + || !isInRange(kickOption, 0, MAX_OPTION_LEVEL) + || !isInRange(banOption, 0, MAX_OPTION_LEVEL) + || !isInRange(chatMode, 0, MAX_OPTION_LEVEL) + || !isInRange(chatWeight, 0, MAX_OPTION_LEVEL) + || !isInRange(chatSpeed, 0, MAX_OPTION_LEVEL) + || !isInRange(chatDistance, MIN_CHAT_DISTANCE, MAX_CHAT_DISTANCE) + || !isInRange(chatProtection, 0, MAX_OPTION_LEVEL)) { + return; + } + room.setTags(tags.toString()); - room.setTradeMode(this.packet.readInt()); - room.setAllowPets(this.packet.readBoolean()); - room.setAllowPetsEat(this.packet.readBoolean()); - room.setAllowWalkthrough(this.packet.readBoolean()); - room.setHideWall(this.packet.readBoolean()); - room.setWallSize(this.packet.readInt()); - room.setFloorSize(this.packet.readInt()); - room.setMuteOption(this.packet.readInt()); - room.setKickOption(this.packet.readInt()); - room.setBanOption(this.packet.readInt()); - room.setChatMode(this.packet.readInt()); - room.setChatWeight(this.packet.readInt()); - room.setChatSpeed(this.packet.readInt()); - room.setChatDistance(Math.abs(this.packet.readInt())); - room.setChatProtection(this.packet.readInt()); + room.setTradeMode(tradeMode); + room.setAllowPets(allowPets); + room.setAllowPetsEat(allowPetsEat); + room.setAllowWalkthrough(allowWalkthrough); + room.setHideWall(hideWall); + room.setWallSize(wallSize); + room.setFloorSize(floorSize); + room.setMuteOption(muteOption); + room.setKickOption(kickOption); + room.setBanOption(banOption); + room.setChatMode(chatMode); + room.setChatWeight(chatWeight); + room.setChatSpeed(chatSpeed); + room.setChatDistance(chatDistance); + room.setChatProtection(chatProtection); if (this.packet.bytesAvailable() > 0) { room.setAllowUnderpass(this.packet.readBoolean()); @@ -144,4 +197,8 @@ public class RoomSettingsSaveEvent extends MessageHandler { } } } + + private static boolean isInRange(int value, int min, int max) { + return value >= min && value <= max; + } } diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/RoomSettingsInputGuardContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/RoomSettingsInputGuardContractTest.java new file mode 100644 index 00000000..ae93f7ef --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/RoomSettingsInputGuardContractTest.java @@ -0,0 +1,60 @@ +package com.eu.habbo.messages.incoming.rooms; + +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 RoomSettingsInputGuardContractTest { + private static String source() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/RoomSettingsSaveEvent.java")); + } + + @Test + void roomStateAndTagCountAreValidatedWithoutModuloOrTruncation() throws Exception { + String source = source(); + + int stateRead = source.indexOf("int stateId = this.packet.readInt()"); + int stateGuard = source.indexOf("stateId < 0 || stateId >= RoomState.values().length", stateRead); + int stateAssign = source.indexOf("RoomState state = RoomState.values()[stateId]", stateGuard); + int tagCount = source.indexOf("int count = this.packet.readInt()", stateAssign); + int tagGuard = source.indexOf("count < 0 || count > MAX_TAGS", tagCount); + int tagLoop = source.indexOf("for (int i = 0; i < count; i++)", tagGuard); + + assertTrue(source.contains("MAX_TAGS = 2"), "Room settings tag count should have an explicit cap"); + assertTrue(!source.contains("% RoomState.values().length"), + "Room state must not use modulo because negative values can crash or remap input"); + assertTrue(!source.contains("Math.min(this.packet.readInt(), 2)"), + "Room settings must reject oversized tag counts instead of truncating and desynchronizing the packet"); + assertTrue(stateRead > -1 && stateGuard > stateRead && stateAssign > stateGuard, + "Room state must be range-checked before indexing RoomState.values()"); + assertTrue(tagCount > -1 && tagGuard > tagCount && tagLoop > tagGuard, + "Tag count must be range-checked before reading tag strings"); + } + + @Test + void roomSettingsOptionsAreValidatedBeforeMutatingRoom() throws Exception { + String source = source(); + + int tradeMode = source.indexOf("int tradeMode = this.packet.readInt()"); + int validation = source.indexOf("!isInRange(tradeMode, 0, MAX_OPTION_LEVEL)", tradeMode); + int setTags = source.indexOf("room.setTags(tags.toString())", validation); + int setChatDistance = source.indexOf("room.setChatDistance(chatDistance)", setTags); + + assertTrue(source.contains("MAX_ROOM_PASSWORD_LENGTH = 64"), + "Room password should have a bounded server-side length"); + assertTrue(source.contains("MAX_USERS_MAX = 200"), + "Room capacity should have a bounded server-side maximum"); + assertTrue(source.contains("MIN_CHAT_DISTANCE = 1") && source.contains("MAX_CHAT_DISTANCE = 99"), + "Room chat distance should be explicitly bounded"); + assertTrue(!source.contains("Math.abs(this.packet.readInt())"), + "Room settings must reject invalid chat distance instead of converting negative values"); + assertTrue(validation > tradeMode, "Room options must be validated after reading them"); + assertTrue(validation < setTags, "Room options must be validated before mutating room fields"); + assertTrue(setChatDistance > setTags, "Validated chat distance should be applied after the guard block"); + assertTrue(source.contains("private static boolean isInRange"), + "Room settings should use one clear range helper for numeric option guards"); + } +} From b0d4317c2d1f1ad05fe1b2dd8ffcb029c722e8fa Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 20:21:54 +0200 Subject: [PATCH 07/18] fix(catalog): repair search offer ids --- .../habbo/habbohotel/catalog/CatalogItem.java | 8 +++ .../habbohotel/catalog/CatalogManager.java | 7 ++- .../catalog/CatalogSearchedItemEvent.java | 13 +++-- .../CatalogSearchOfferIdContractTest.java | 55 +++++++++++++++++++ 4 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/CatalogSearchOfferIdContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogItem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogItem.java index 7bfa22f8..8c9746dc 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogItem.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogItem.java @@ -174,6 +174,14 @@ public class CatalogItem implements ISerialize, Runnable, Comparable 0) { + return this.offerId; + } + + return haveOffer(this) ? this.id : -1; + } + public boolean isLimited() { return this.limitedStack > 0; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java index 29e6ff11..86dfb6c1 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java @@ -494,10 +494,11 @@ public class CatalogManager { item = new CatalogItem(set); page.addItem(item); - if (item.getOfferId() != -1) { - page.addOfferId(item.getOfferId()); + int searchOfferId = item.getSearchOfferId(); + if (searchOfferId != -1) { + page.addOfferId(searchOfferId); - this.offerDefs.put(item.getOfferId(), item.getId()); + this.offerDefs.put(searchOfferId, item.getId()); } } else item.update(set); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogSearchedItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogSearchedItemEvent.java index 0851e03e..4c02cc5a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogSearchedItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogSearchedItemEvent.java @@ -12,10 +12,15 @@ public class CatalogSearchedItemEvent extends MessageHandler { public void handle() throws Exception { int offerId = this.packet.readInt(); - int pageId = Emulator.getGameEnvironment().getCatalogManager().offerDefs.get(offerId); + int catalogItemId = Emulator.getGameEnvironment().getCatalogManager().offerDefs.get(offerId); - if (pageId != 0) { - CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(Emulator.getGameEnvironment().getCatalogManager().getCatalogItem(pageId).getPageId()); + if (catalogItemId != 0) { + CatalogItem requestedItem = Emulator.getGameEnvironment().getCatalogManager().getCatalogItem(catalogItemId); + if (requestedItem == null) { + return; + } + + CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(requestedItem.getPageId()); if (page != null) { TIntObjectIterator iterator = page.getCatalogItems().iterator(); @@ -25,7 +30,7 @@ public class CatalogSearchedItemEvent extends MessageHandler { CatalogItem item = iterator.value(); - if (item.getOfferId() == offerId) { + if (item.getSearchOfferId() == offerId) { this.client.sendResponse(new CatalogSearchResultComposer(item)); return; } diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/CatalogSearchOfferIdContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/CatalogSearchOfferIdContractTest.java new file mode 100644 index 00000000..b738723a --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/CatalogSearchOfferIdContractTest.java @@ -0,0 +1,55 @@ +package com.eu.habbo.messages.incoming.catalog; + +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 CatalogSearchOfferIdContractTest { + private static String source(String path) throws Exception { + return Files.readString(Path.of(path)); + } + + @Test + void catalogItemsExposeStableSearchOfferIdWhenDatabaseOfferIdIsMissing() throws Exception { + String source = source("src/main/java/com/eu/habbo/habbohotel/catalog/CatalogItem.java"); + + int method = source.indexOf("public int getSearchOfferId()"); + int rawGuard = source.indexOf("this.offerId > 0", method); + int fallback = source.indexOf("return haveOffer(this) ? this.id : -1", rawGuard); + + assertTrue(method > -1, "CatalogItem should expose a search-safe offer id"); + assertTrue(rawGuard > method, "CatalogItem should preserve valid positive database offer ids"); + assertTrue(fallback > rawGuard, + "CatalogItem should fall back to catalog item id when offer_id is missing but the item can be offered"); + } + + @Test + void catalogManagerIndexesSearchOfferIdsInsteadOfRawOfferIds() throws Exception { + String source = source("src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java"); + + int searchOffer = source.indexOf("int searchOfferId = item.getSearchOfferId()"); + int addOffer = source.indexOf("page.addOfferId(searchOfferId)", searchOffer); + int offerDefs = source.indexOf("this.offerDefs.put(searchOfferId, item.getId())", addOffer); + + assertTrue(searchOffer > -1, "CatalogManager should calculate the runtime search offer id"); + assertTrue(addOffer > searchOffer, "CatalogManager should expose runtime search offer ids in catalog pages"); + assertTrue(offerDefs > addOffer, "CatalogManager should map runtime search offer ids back to catalog items"); + assertTrue(!source.contains("this.offerDefs.put(item.getOfferId(), item.getId())"), + "CatalogManager must not index raw -1 offer ids for catalog search"); + } + + @Test + void catalogSearchLookupResolvesCatalogItemIdsAndComparesSearchOfferIds() throws Exception { + String source = source("src/main/java/com/eu/habbo/messages/incoming/catalog/CatalogSearchedItemEvent.java"); + + assertTrue(source.contains("int catalogItemId ="), + "Catalog search lookup should name offerDefs values as catalog item ids"); + assertTrue(source.contains("getCatalogItem(catalogItemId)"), + "Catalog search should resolve the mapped catalog item directly"); + assertTrue(source.contains("item.getSearchOfferId() == offerId"), + "Catalog search should compare runtime search offer ids, not raw database offer ids"); + } +} From 736b7c70b4f8ab650332f6aca3d8f88f6686751f Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 20:26:19 +0200 Subject: [PATCH 08/18] fix(polls): bound answer payloads --- .../incoming/polls/AnswerPollEvent.java | 11 +++- .../PollAnswerInputGuardContractTest.java | 53 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/polls/PollAnswerInputGuardContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/polls/AnswerPollEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/polls/AnswerPollEvent.java index a419f63d..d8b069b4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/polls/AnswerPollEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/polls/AnswerPollEvent.java @@ -16,6 +16,9 @@ import java.sql.SQLException; public class AnswerPollEvent extends MessageHandler { private static final Logger LOGGER = LoggerFactory.getLogger(AnswerPollEvent.class); + private static final int MAX_ANSWER_COUNT = 20; + private static final int MAX_ANSWER_PART_LENGTH = 255; + private static final int MAX_COMBINED_ANSWER_LENGTH = 2048; @Override public void handle() throws Exception { @@ -23,13 +26,19 @@ public class AnswerPollEvent extends MessageHandler { int questionId = this.packet.readInt(); int count = this.packet.readInt(); String answers = this.packet.readString(); + if (count <= 0 || count > MAX_ANSWER_COUNT || answers == null || answers.length() > MAX_ANSWER_PART_LENGTH) { + return; + } StringBuilder answer = new StringBuilder(); for (int i = 0; i < count; i++) { answer.append(":").append(answers); + if (answer.length() > MAX_COMBINED_ANSWER_LENGTH) { + return; + } } - if(answer.length() <= 0) return; + if (answer.length() <= 0) return; if (pollId == 0 && questionId <= 0) { Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/polls/PollAnswerInputGuardContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/polls/PollAnswerInputGuardContractTest.java new file mode 100644 index 00000000..afc6f516 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/polls/PollAnswerInputGuardContractTest.java @@ -0,0 +1,53 @@ +package com.eu.habbo.messages.incoming.polls; + +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 PollAnswerInputGuardContractTest { + private static String source() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/polls/AnswerPollEvent.java")); + } + + @Test + void pollAnswerCountAndPartLengthAreBoundedBeforeBuildingCombinedAnswer() throws Exception { + String source = source(); + + int count = source.indexOf("int count = this.packet.readInt()"); + int answers = source.indexOf("String answers = this.packet.readString()", count); + int guard = source.indexOf("count <= 0 || count > MAX_ANSWER_COUNT", answers); + int builder = source.indexOf("StringBuilder answer = new StringBuilder()", guard); + int loop = source.indexOf("for (int i = 0; i < count; i++)", builder); + + assertTrue(source.contains("MAX_ANSWER_COUNT = 20"), + "Poll answers should have a bounded answer count"); + assertTrue(source.contains("MAX_ANSWER_PART_LENGTH = 255"), + "Poll answer fragments should have a bounded length"); + assertTrue(source.contains("MAX_COMBINED_ANSWER_LENGTH = 2048"), + "Poll combined answer should have a bounded final length"); + assertTrue(count > -1 && answers > count, "Poll handler must read count and answer string"); + assertTrue(guard > answers, "Poll handler must validate count and answer string after reading them"); + assertTrue(guard < builder && builder < loop, + "Poll handler must validate inputs before building the repeated answer string"); + } + + @Test + void combinedAnswerLengthIsCheckedBeforeWordQuizOrDatabaseWrite() throws Exception { + String source = source(); + + int append = source.indexOf("answer.append(\":\").append(answers)"); + int combinedGuard = source.indexOf("answer.length() > MAX_COMBINED_ANSWER_LENGTH", append); + int wordQuiz = source.indexOf("handleWordQuiz", combinedGuard); + int dbWrite = source.indexOf("INSERT INTO polls_answers", combinedGuard); + + assertTrue(combinedGuard > append, + "Poll handler must check combined answer length while building it"); + assertTrue(combinedGuard < wordQuiz, + "Poll handler must bound word quiz answers before dispatching them"); + assertTrue(combinedGuard < dbWrite, + "Poll handler must bound poll answers before persisting them"); + } +} From 112796e133a5b74dc53d6bcf2fc379541b39ccb1 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 20:29:26 +0200 Subject: [PATCH 09/18] fix(guilds): bound guild management inputs --- .../habbo/habbohotel/guilds/GuildManager.java | 2 +- .../guilds/GuildChangeColorsEvent.java | 11 +++- .../guilds/GuildChangeNameDescEvent.java | 6 +- .../guilds/GuildChangeSettingsEvent.java | 10 +++- .../incoming/guilds/GuildInputLimits.java | 17 ++++++ .../incoming/guilds/RequestGuildBuyEvent.java | 9 ++- .../guilds/RequestGuildMembersEvent.java | 7 +++ ...GuildManagementInputGuardContractTest.java | 59 +++++++++++++++++++ .../GuildMembersInputGuardContractTest.java | 55 +++++++++++++++++ 9 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildInputLimits.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/guilds/GuildManagementInputGuardContractTest.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/guilds/GuildMembersInputGuardContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/GuildManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/GuildManager.java index 9441fc11..8f1cfbd3 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/GuildManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/GuildManager.java @@ -441,7 +441,7 @@ public class GuildManager { public int getGuildMembersCount(Guild guild, int page, int levelId, String query) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM guilds_members INNER JOIN users ON guilds_members.user_id = users.id WHERE guilds_members.guild_id = ? " + (rankQuery(levelId)) + " AND users.username LIKE ? ORDER BY level_id, member_since ASC")) { statement.setInt(1, guild.getId()); - statement.setString(2, "%" + query + "%"); + statement.setString(2, "%" + com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%"); try (ResultSet set = statement.executeQuery()) { while (set.next()) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeColorsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeColorsEvent.java index d184af1b..aa6246e2 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeColorsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeColorsEvent.java @@ -21,12 +21,21 @@ public class GuildChangeColorsEvent extends MessageHandler { if (guild != null) { if (guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_GUILD_ADMIN)) { - GuildChangedColorsEvent colorsEvent = new GuildChangedColorsEvent(guild, this.packet.readInt(), this.packet.readInt()); + int colorOne = this.packet.readInt(); + int colorTwo = this.packet.readInt(); + + if (!Emulator.getGameEnvironment().getGuildManager().symbolColor(colorOne) || !Emulator.getGameEnvironment().getGuildManager().backgroundColor(colorTwo)) + return; + + GuildChangedColorsEvent colorsEvent = new GuildChangedColorsEvent(guild, colorOne, colorTwo); Emulator.getPluginManager().fireEvent(colorsEvent); if (colorsEvent.isCancelled()) return; + if (!Emulator.getGameEnvironment().getGuildManager().symbolColor(colorsEvent.colorOne) || !Emulator.getGameEnvironment().getGuildManager().backgroundColor(colorsEvent.colorTwo)) + return; + if (guild.getColorOne() != colorsEvent.colorOne || guild.getColorTwo() != colorsEvent.colorTwo) { guild.setColorOne(colorsEvent.colorOne); guild.setColorTwo(colorsEvent.colorTwo); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeNameDescEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeNameDescEvent.java index b1fbefcf..d1adec2c 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeNameDescEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeNameDescEvent.java @@ -23,6 +23,10 @@ public class GuildChangeNameDescEvent extends MessageHandler { if (guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_GUILD_ADMIN)) { String newName = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo()); String newDesc = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo()); + + if (!GuildInputLimits.isValidGuildName(newName) || !GuildInputLimits.isValidGuildDescription(newDesc)) + return; + GuildChangedNameEvent nameEvent = new GuildChangedNameEvent(guild, newName, newDesc); Emulator.getPluginManager().fireEvent(nameEvent); @@ -32,7 +36,7 @@ public class GuildChangeNameDescEvent extends MessageHandler { if (guild.getName().equals(nameEvent.name) && guild.getDescription().equals(nameEvent.description)) return; - if(nameEvent.name.length() > 29 || nameEvent.description.length() > 254) + if (!GuildInputLimits.isValidGuildName(nameEvent.name) || !GuildInputLimits.isValidGuildDescription(nameEvent.description)) return; guild.setName(nameEvent.name); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeSettingsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeSettingsEvent.java index 5ad35f21..84ebbf72 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeSettingsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeSettingsEvent.java @@ -40,12 +40,20 @@ public class GuildChangeSettingsEvent extends MessageHandler { if (guild != null) { if (guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || this.client.getHabbo().hasPermission(Permission.ACC_GUILD_ADMIN)) { - GuildChangedSettingsEvent settingsEvent = new GuildChangedSettingsEvent(guild, this.packet.readInt(), this.packet.readInt() == 0); + int state = this.packet.readInt(); + + if (state < 0 || state >= GuildState.values().length) + return; + + GuildChangedSettingsEvent settingsEvent = new GuildChangedSettingsEvent(guild, state, this.packet.readInt() == 0); Emulator.getPluginManager().fireEvent(settingsEvent); if (settingsEvent.isCancelled()) return; + if (settingsEvent.state < 0 || settingsEvent.state >= GuildState.values().length) + return; + guild.setState(GuildState.valueOf(settingsEvent.state)); guild.setRights(settingsEvent.rights); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildInputLimits.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildInputLimits.java new file mode 100644 index 00000000..c860b7cd --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildInputLimits.java @@ -0,0 +1,17 @@ +package com.eu.habbo.messages.incoming.guilds; + +final class GuildInputLimits { + static final int MAX_GUILD_NAME_LENGTH = 29; + static final int MAX_GUILD_DESCRIPTION_LENGTH = 254; + + private GuildInputLimits() { + } + + static boolean isValidGuildName(String name) { + return name != null && !name.isBlank() && name.length() <= MAX_GUILD_NAME_LENGTH; + } + + static boolean isValidGuildDescription(String description) { + return description != null && description.length() <= MAX_GUILD_DESCRIPTION_LENGTH; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildBuyEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildBuyEvent.java index 1b7a8c0b..31cd489a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildBuyEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildBuyEvent.java @@ -31,11 +31,11 @@ public class RequestGuildBuyEvent extends MessageHandler { final String name = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo()); final String description = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo()); - if (name.length() == 0 || name.length() > 29) { + if (!GuildInputLimits.isValidGuildName(name)) { this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.INVALID_GUILD_NAME)); return; } - if (description.length() > 254) { + if (!GuildInputLimits.isValidGuildDescription(description)) { return; } @@ -68,6 +68,11 @@ public class RequestGuildBuyEvent extends MessageHandler { int colorOne = this.packet.readInt(); int colorTwo = this.packet.readInt(); + if (!Emulator.getGameEnvironment().getGuildManager().symbolColor(colorOne) || !Emulator.getGameEnvironment().getGuildManager().backgroundColor(colorTwo)) { + this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.INVALID_GUILD_NAME)); + return; + } + int count = this.packet.readInt(); String badge = GuildBadgeBuilder.readBadge(this.packet, count); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildMembersEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildMembersEvent.java index e9c218ca..9452d7cc 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildMembersEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildMembersEvent.java @@ -9,6 +9,10 @@ import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.guilds.GuildMembersComposer; public class RequestGuildMembersEvent extends MessageHandler { + private static final int MAX_PAGE_ID = 1000; + private static final int MAX_QUERY_LENGTH = 32; + private static final int MAX_LEVEL_ID = 2; + @Override public int getRatelimit() { return 500; @@ -20,6 +24,9 @@ public class RequestGuildMembersEvent extends MessageHandler { int pageId = this.packet.readInt(); String query = this.packet.readString(); int levelId = this.packet.readInt(); + if (pageId < 0 || pageId > MAX_PAGE_ID || levelId < 0 || levelId > MAX_LEVEL_ID || query == null || query.length() > MAX_QUERY_LENGTH) { + return; + } Guild g = Emulator.getGameEnvironment().getGuildManager().getGuild(groupId); diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/guilds/GuildManagementInputGuardContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/guilds/GuildManagementInputGuardContractTest.java new file mode 100644 index 00000000..425f5327 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/guilds/GuildManagementInputGuardContractTest.java @@ -0,0 +1,59 @@ +package com.eu.habbo.messages.incoming.guilds; + +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 GuildManagementInputGuardContractTest { + private static String source(String file) throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/guilds/" + file)); + } + + @Test + void guildCreateAndRenameShareNameAndDescriptionBounds() throws Exception { + String limits = source("GuildInputLimits.java"); + String buy = source("RequestGuildBuyEvent.java"); + String rename = source("GuildChangeNameDescEvent.java"); + + assertTrue(limits.contains("MAX_GUILD_NAME_LENGTH = 29"), + "Guild names should keep the existing 29 character protocol bound"); + assertTrue(limits.contains("MAX_GUILD_DESCRIPTION_LENGTH = 254"), + "Guild descriptions should keep the existing database/protocol bound"); + assertTrue(limits.contains("!name.isBlank()"), + "Guild names must not be empty or whitespace-only"); + assertTrue(buy.contains("GuildInputLimits.isValidGuildName(name)"), + "Guild purchase should use the shared name guard"); + assertTrue(buy.contains("GuildInputLimits.isValidGuildDescription(description)"), + "Guild purchase should use the shared description guard"); + assertTrue(rename.contains("GuildInputLimits.isValidGuildName(newName)"), + "Guild rename should reject invalid client names before plugin events"); + assertTrue(rename.contains("GuildInputLimits.isValidGuildName(nameEvent.name)"), + "Guild rename should reject invalid plugin-mutated names before persistence"); + } + + @Test + void guildColorInputsAreCheckedAgainstLoadedPalette() throws Exception { + String buy = source("RequestGuildBuyEvent.java"); + String colors = source("GuildChangeColorsEvent.java"); + + assertTrue(buy.contains("symbolColor(colorOne)") && buy.contains("backgroundColor(colorTwo)"), + "Guild purchase should reject color ids that are not in the loaded guild palette"); + assertTrue(colors.contains("symbolColor(colorOne)") && colors.contains("backgroundColor(colorTwo)"), + "Guild color changes should reject invalid client color ids"); + assertTrue(colors.contains("symbolColor(colorsEvent.colorOne)") && colors.contains("backgroundColor(colorsEvent.colorTwo)"), + "Guild color changes should reject invalid plugin-mutated color ids"); + } + + @Test + void guildStateInputsStayInsideKnownEnumRange() throws Exception { + String settings = source("GuildChangeSettingsEvent.java"); + + assertTrue(settings.contains("state < 0 || state >= GuildState.values().length"), + "Guild settings should reject invalid client state ids before event dispatch"); + assertTrue(settings.contains("settingsEvent.state < 0 || settingsEvent.state >= GuildState.values().length"), + "Guild settings should reject invalid plugin-mutated state ids before applying them"); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/guilds/GuildMembersInputGuardContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/guilds/GuildMembersInputGuardContractTest.java new file mode 100644 index 00000000..31b9c70e --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/guilds/GuildMembersInputGuardContractTest.java @@ -0,0 +1,55 @@ +package com.eu.habbo.messages.incoming.guilds; + +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 GuildMembersInputGuardContractTest { + private static String eventSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/guilds/RequestGuildMembersEvent.java")); + } + + private static String managerSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/guilds/GuildManager.java")); + } + + @Test + void guildMemberListInputsAreBoundedBeforeManagerQueries() throws Exception { + String source = eventSource(); + + int pageRead = source.indexOf("int pageId = this.packet.readInt()"); + int queryRead = source.indexOf("String query = this.packet.readString()", pageRead); + int levelRead = source.indexOf("int levelId = this.packet.readInt()", queryRead); + int guard = source.indexOf("pageId < 0 || pageId > MAX_PAGE_ID", levelRead); + int managerCall = source.indexOf("getGuildMembers(g, pageId, levelId, query)", guard); + + assertTrue(source.contains("MAX_PAGE_ID = 1000"), + "Guild member pagination should have a server-side upper bound"); + assertTrue(source.contains("MAX_QUERY_LENGTH = 32"), + "Guild member search query should have a server-side length bound"); + assertTrue(source.contains("MAX_LEVEL_ID = 2"), + "Guild member rank filter should be bounded to known levels"); + assertTrue(pageRead > -1 && queryRead > pageRead && levelRead > queryRead, + "Guild member handler must read page/query/level from the packet"); + assertTrue(guard > levelRead && guard < managerCall, + "Guild member handler must validate inputs before querying the manager"); + } + + @Test + void guildMemberCountEscapesLikeQuerySameAsListQuery() throws Exception { + String source = managerSource(); + + int listMethod = source.indexOf("public ArrayList getGuildMembers(Guild guild, int page, int levelId, String query)"); + int listEscape = source.indexOf("SqlLikeEscaper.escape(query)", listMethod); + int countMethod = source.indexOf("public int getGuildMembersCount(Guild guild, int page, int levelId, String query)"); + int countEscape = source.indexOf("SqlLikeEscaper.escape(query)", countMethod); + + assertTrue(listEscape > listMethod, + "Guild member list query should escape SQL LIKE wildcards"); + assertTrue(countEscape > countMethod, + "Guild member count query should escape SQL LIKE wildcards too"); + } +} From 9b9902c76d235b1e77395669167ae5dfd1647476 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 20:37:48 +0200 Subject: [PATCH 10/18] fix(forums): bound guild forum view inputs --- .../guilds/forums/GuildForumDataEvent.java | 6 ++++++ .../guilds/forums/GuildForumInputGuard.java | 5 +++++ .../guilds/forums/GuildForumMarkAsReadEvent.java | 14 ++++++++++++++ .../guilds/forums/GuildForumThreadsEvent.java | 2 +- .../forums/GuildForumInputGuardContractTest.java | 16 ++++++++++++++++ .../guilds/forums/GuildForumInputGuardTest.java | 2 ++ 6 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumDataEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumDataEvent.java index d1d64e82..0c821d5a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumDataEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumDataEvent.java @@ -8,6 +8,7 @@ import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer; +import com.eu.habbo.messages.outgoing.handshake.ConnectionErrorComposer; public class GuildForumDataEvent extends MessageHandler { @Override @@ -19,6 +20,11 @@ public class GuildForumDataEvent extends MessageHandler { public void handle() throws Exception { int guildId = packet.readInt(); + if (!GuildForumInputGuard.isPositiveId(guildId)) { + this.client.sendResponse(new ConnectionErrorComposer(400)); + return; + } + Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId); if (guild == null) return; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumInputGuard.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumInputGuard.java index 36f8876f..d81eec50 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumInputGuard.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumInputGuard.java @@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.guilds.forums; final class GuildForumInputGuard { static final int MAX_PAGE_LIMIT = 50; + static final int MAX_THREAD_INDEX = 1000; static final int MAX_MARK_READ_BATCH = 50; private GuildForumInputGuard() { @@ -19,6 +20,10 @@ final class GuildForumInputGuard { return index >= 0 && limit > 0 && limit <= MAX_PAGE_LIMIT; } + static boolean isValidThreadIndex(int index) { + return index >= 0 && index <= MAX_THREAD_INDEX; + } + static boolean isValidMarkReadBatch(int count) { return count > 0 && count <= MAX_MARK_READ_BATCH; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumMarkAsReadEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumMarkAsReadEvent.java index fa83390b..0dfca47f 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumMarkAsReadEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumMarkAsReadEvent.java @@ -1,6 +1,9 @@ package com.eu.habbo.messages.incoming.guilds.forums; import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.guilds.Guild; +import com.eu.habbo.habbohotel.guilds.GuildMember; +import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer; import org.slf4j.Logger; @@ -37,6 +40,17 @@ public class GuildForumMarkAsReadEvent extends MessageHandler { continue; } + Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId); + if (guild == null || !guild.hasForum()) { + continue; + } + + GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, userId); + boolean staff = this.client.getHabbo().hasPermission(Permission.ACC_MODTOOL_TICKET_Q); + if (!guild.canHabboReadForum(userId, member, staff)) { + continue; + } + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( "INSERT INTO `guild_forum_views` (`user_id`, `guild_id`, `timestamp`) VALUES (?, ?, ?) " + "ON DUPLICATE KEY UPDATE `timestamp` = ?" diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadsEvent.java index b4c4cfe6..c7878663 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadsEvent.java @@ -22,7 +22,7 @@ public class GuildForumThreadsEvent extends MessageHandler { int guildId = packet.readInt(); int index = packet.readInt(); - if (!GuildForumInputGuard.isPositiveId(guildId) || index < 0) { + if (!GuildForumInputGuard.isPositiveId(guildId) || !GuildForumInputGuard.isValidThreadIndex(index)) { this.client.sendResponse(new ConnectionErrorComposer(400)); return; } diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumInputGuardContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumInputGuardContractTest.java index d7c32f8b..11bc5330 100644 --- a/Emulator/src/test/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumInputGuardContractTest.java +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumInputGuardContractTest.java @@ -15,6 +15,7 @@ class GuildForumInputGuardContractTest { for (String handler : List.of( "GuildForumPostThreadEvent.java", + "GuildForumDataEvent.java", "GuildForumModerateMessageEvent.java", "GuildForumModerateThreadEvent.java", "GuildForumThreadUpdateEvent.java", @@ -39,9 +40,12 @@ class GuildForumInputGuardContractTest { String settings = Files.readString(base.resolve("GuildForumUpdateSettingsEvent.java")); String moderateThread = Files.readString(base.resolve("GuildForumModerateThreadEvent.java")); String moderateMessage = Files.readString(base.resolve("GuildForumModerateMessageEvent.java")); + String threads = Files.readString(base.resolve("GuildForumThreadsEvent.java")); assertTrue(messages.contains("GuildForumInputGuard.isValidPage(index, limit)"), "thread message reads must bound index/limit before fetching comments"); + assertTrue(threads.contains("GuildForumInputGuard.isValidThreadIndex(index)"), + "thread list reads must bound the client-provided index before composing results"); assertTrue(markRead.contains("GuildForumInputGuard.isValidMarkReadBatch(count)"), "mark-as-read must bound the client-provided batch count before DB writes"); assertTrue(settings.contains("GuildForumInputGuard.isSettingsState"), @@ -59,4 +63,16 @@ class GuildForumInputGuardContractTest { assertTrue(source.contains("GuildForumInputGuard.normalize(this.packet.readString())"), "forum post subject and body should be normalized before word filtering and length checks"); } + + @Test + void markAsReadRequiresForumReadAccessBeforeWritingViews() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumMarkAsReadEvent.java")); + + int guildLookup = source.indexOf("Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId)"); + int readGuard = source.indexOf("guild.canHabboReadForum(userId, member, staff)"); + int insert = source.indexOf("INSERT INTO `guild_forum_views`"); + + assertTrue(guildLookup > -1 && readGuard > guildLookup && readGuard < insert, + "mark-as-read should confirm the user can read the forum before inserting view rows"); + } } diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumInputGuardTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumInputGuardTest.java index 47189ba6..e39bff1c 100644 --- a/Emulator/src/test/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumInputGuardTest.java +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumInputGuardTest.java @@ -21,6 +21,8 @@ class GuildForumInputGuardTest { assertFalse(GuildForumInputGuard.isValidPage(0, 0)); assertTrue(GuildForumInputGuard.isValidPage(0, GuildForumInputGuard.MAX_PAGE_LIMIT)); assertFalse(GuildForumInputGuard.isValidPage(0, GuildForumInputGuard.MAX_PAGE_LIMIT + 1)); + assertTrue(GuildForumInputGuard.isValidThreadIndex(GuildForumInputGuard.MAX_THREAD_INDEX)); + assertFalse(GuildForumInputGuard.isValidThreadIndex(GuildForumInputGuard.MAX_THREAD_INDEX + 1)); } @Test From 26f86e3e31b79fc28cb900d832829bfbf9059ba9 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 19:59:52 +0200 Subject: [PATCH 11/18] fix(friends): bound messenger inputs --- .../friends/AcceptFriendRequestEvent.java | 8 +- .../incoming/friends/ChangeRelationEvent.java | 15 ++- .../friends/DeclineFriendRequestEvent.java | 5 +- .../incoming/friends/FriendInputGuard.java | 31 +++++ .../friends/FriendPrivateMessageEvent.java | 8 +- .../incoming/friends/FriendRequestEvent.java | 10 +- .../incoming/friends/InviteFriendsEvent.java | 7 +- .../incoming/friends/RemoveFriendEvent.java | 5 + .../friends/FriendBatchGuardContractTest.java | 107 ++++++++++++++++++ 9 files changed, 181 insertions(+), 15 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/FriendInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/friends/FriendBatchGuardContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/AcceptFriendRequestEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/AcceptFriendRequestEvent.java index 6017b08d..73d098ae 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/AcceptFriendRequestEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/AcceptFriendRequestEvent.java @@ -25,14 +25,16 @@ public class AcceptFriendRequestEvent extends MessageHandler { @Override public void handle() throws Exception { - int count = Math.min(this.packet.readInt(), 100); + int count = this.packet.readInt(); + if (count <= 0 || count > 100) return; + int userId; for (int i = 0; i < count; i++) { userId = this.packet.readInt(); - if (userId == 0) - return; + if (userId <= 0) + continue; if (this.client.getHabbo().getMessenger().getFriends().containsKey(userId)) { this.client.getHabbo().getMessenger().deleteFriendRequests(userId, this.client.getHabbo().getHabboInfo().getId()); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/ChangeRelationEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/ChangeRelationEvent.java index d2790194..b39289b3 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/ChangeRelationEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/ChangeRelationEvent.java @@ -1,5 +1,6 @@ package com.eu.habbo.messages.incoming.friends; +import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.messenger.MessengerBuddy; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.friends.UpdateFriendComposer; @@ -12,12 +13,16 @@ public class ChangeRelationEvent extends MessageHandler { int relationId = this.packet.readInt(); MessengerBuddy buddy = this.client.getHabbo().getMessenger().getFriends().get(userId); - if (buddy != null && relationId >= 0 && relationId <= 3) { + if (buddy != null && FriendInputGuard.isValidRelation(relationId)) { UserRelationShipEvent event = new UserRelationShipEvent(this.client.getHabbo(), buddy, relationId); - if (!event.isCancelled()) { - buddy.setRelation(event.relationShip); - this.client.sendResponse(new UpdateFriendComposer(this.client.getHabbo(), buddy, 0)); - } + if (Emulator.getPluginManager().fireEvent(event).isCancelled()) + return; + + if (!FriendInputGuard.isValidRelation(event.relationShip)) + return; + + buddy.setRelation(event.relationShip); + this.client.sendResponse(new UpdateFriendComposer(this.client.getHabbo(), buddy, 0)); } } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/DeclineFriendRequestEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/DeclineFriendRequestEvent.java index 8010cf5f..2915ae09 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/DeclineFriendRequestEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/DeclineFriendRequestEvent.java @@ -3,6 +3,8 @@ package com.eu.habbo.messages.incoming.friends; import com.eu.habbo.messages.incoming.MessageHandler; public class DeclineFriendRequestEvent extends MessageHandler { + private static final int MAX_BATCH_SIZE = 100; + @Override public void handle() throws Exception { boolean all = this.packet.readBoolean(); @@ -11,10 +13,11 @@ public class DeclineFriendRequestEvent extends MessageHandler { this.client.getHabbo().getMessenger().deleteAllFriendRequests(this.client.getHabbo().getHabboInfo().getId()); } else { int count = this.packet.readInt(); + if (count <= 0 || count > MAX_BATCH_SIZE) return; for (int i = 0; i < count; i++) { this.client.getHabbo().getMessenger().deleteFriendRequests(this.packet.readInt(), this.client.getHabbo().getHabboInfo().getId()); } } } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/FriendInputGuard.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/FriendInputGuard.java new file mode 100644 index 00000000..0267c884 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/FriendInputGuard.java @@ -0,0 +1,31 @@ +package com.eu.habbo.messages.incoming.friends; + +final class FriendInputGuard { + static final int MAX_USERNAME_LENGTH = 15; + static final int MAX_MESSAGE_LENGTH = 255; + static final int MAX_RELATION_ID = 3; + + private FriendInputGuard() { + } + + static String normalizeUsername(String username) { + return username == null ? "" : username.trim(); + } + + static boolean isValidUsername(String username) { + return username != null && !username.isBlank() && username.length() <= MAX_USERNAME_LENGTH; + } + + static String normalizeMessage(String message) { + if (message == null) { + return ""; + } + + String normalized = message.trim(); + return normalized.length() > MAX_MESSAGE_LENGTH ? normalized.substring(0, MAX_MESSAGE_LENGTH) : normalized; + } + + static boolean isValidRelation(int relationId) { + return relationId >= 0 && relationId <= MAX_RELATION_ID; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/FriendPrivateMessageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/FriendPrivateMessageEvent.java index 6145e722..68066fc8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/FriendPrivateMessageEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/FriendPrivateMessageEvent.java @@ -9,7 +9,11 @@ public class FriendPrivateMessageEvent extends MessageHandler { @Override public void handle() throws Exception { int userId = this.packet.readInt(); - String message = this.packet.readString(); + String message = FriendInputGuard.normalizeMessage(this.packet.readString()); + + if (message.isEmpty()) { + return; + } if (!this.client.getHabbo().getHabboStats().allowTalk()) { return; @@ -25,8 +29,6 @@ public class FriendPrivateMessageEvent extends MessageHandler { if (buddy == null) return; - if (message.length() > 255) message = message.substring(0, 255); - UserFriendChatEvent event = new UserFriendChatEvent(this.client.getHabbo(), buddy, message); if (Emulator.getPluginManager().fireEvent(event).isCancelled()) return; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/FriendRequestEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/FriendRequestEvent.java index 24afc88b..1fb93fdc 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/FriendRequestEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/FriendRequestEvent.java @@ -26,9 +26,9 @@ public class FriendRequestEvent extends MessageHandler { @Override public void handle() throws Exception { - String username = this.packet.readString(); + String username = FriendInputGuard.normalizeUsername(this.packet.readString()); - if (this.client == null || username == null || username.isEmpty()) + if (this.client == null || !FriendInputGuard.isValidUsername(username)) return; // TargetHabbo can be null if the Habbo is not online or when the Habbo doesn't exist @@ -62,6 +62,12 @@ public class FriendRequestEvent extends MessageHandler { if (targetId == this.client.getHabbo().getHabboInfo().getId()) return; + if (this.client.getHabbo().getMessenger().getFriends().containsKey(targetId)) + return; + + if (Messenger.friendRequested(targetId, this.client.getHabbo().getHabboInfo().getId()) || Messenger.friendRequested(this.client.getHabbo().getHabboInfo().getId(), targetId)) + return; + // Target Habbo exists // Check if Habbo is accepting friend requests if (targetBlocksFriendRequests) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/InviteFriendsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/InviteFriendsEvent.java index 113cecb7..13eb89bd 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/InviteFriendsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/InviteFriendsEvent.java @@ -23,9 +23,14 @@ public class InviteFriendsEvent extends MessageHandler { userIds[i] = this.packet.readInt(); } - String message = this.packet.readString(); + String message = FriendInputGuard.normalizeMessage(this.packet.readString()); + + if (message.isEmpty()) { + return; + } message = Emulator.getGameEnvironment().getWordFilter().filter(message, this.client.getHabbo()); + message = FriendInputGuard.normalizeMessage(message); for (int i : userIds) { if (i == 0) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/RemoveFriendEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/RemoveFriendEvent.java index 27bc05e2..8aea47b1 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/RemoveFriendEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/friends/RemoveFriendEvent.java @@ -8,6 +8,7 @@ import com.eu.habbo.messages.outgoing.friends.RemoveFriendComposer; import gnu.trove.list.array.TIntArrayList; public class RemoveFriendEvent extends MessageHandler { + private static final int MAX_BATCH_SIZE = 100; private final TIntArrayList removedFriends; @@ -18,8 +19,12 @@ public class RemoveFriendEvent extends MessageHandler { @Override public void handle() throws Exception { int count = this.packet.readInt(); + if (count <= 0 || count > MAX_BATCH_SIZE) return; + for (int i = 0; i < count; i++) { int habboId = this.packet.readInt(); + if (habboId <= 0) continue; + this.removedFriends.add(habboId); Messenger.unfriend(this.client.getHabbo().getHabboInfo().getId(), habboId); diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/friends/FriendBatchGuardContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/friends/FriendBatchGuardContractTest.java new file mode 100644 index 00000000..7f7718b1 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/friends/FriendBatchGuardContractTest.java @@ -0,0 +1,107 @@ +package com.eu.habbo.messages.incoming.friends; + +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 FriendBatchGuardContractTest { + private static String source(String name) throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/friends/" + name + ".java")); + } + + @Test + void declineFriendRequestsBoundsClientSuppliedBatchCount() throws Exception { + String source = source("DeclineFriendRequestEvent"); + + int count = source.indexOf("int count = this.packet.readInt()"); + int guard = source.indexOf("count <= 0 || count > MAX_BATCH_SIZE", count); + int loop = source.indexOf("for (int i = 0; i < count; i++)", count); + int delete = source.indexOf("deleteFriendRequests", loop); + + assertTrue(source.contains("MAX_BATCH_SIZE = 100"), + "Friend request decline batches should have a conservative cap"); + assertTrue(count > -1, "DeclineFriendRequestEvent must read the client supplied count"); + assertTrue(guard > count, "DeclineFriendRequestEvent must validate the count after reading it"); + assertTrue(guard < loop, "DeclineFriendRequestEvent must validate the count before looping"); + assertTrue(loop < delete, "DeclineFriendRequestEvent should only mutate after the bounded loop starts"); + } + + @Test + void removeFriendsBoundsClientSuppliedBatchCountBeforeMutations() throws Exception { + String source = source("RemoveFriendEvent"); + + int count = source.indexOf("int count = this.packet.readInt()"); + int guard = source.indexOf("count <= 0 || count > MAX_BATCH_SIZE", count); + int loop = source.indexOf("for (int i = 0; i < count; i++)", count); + int idGuard = source.indexOf("habboId <= 0", loop); + int unfriend = source.indexOf("Messenger.unfriend", loop); + + assertTrue(source.contains("MAX_BATCH_SIZE = 100"), + "Friend removal batches should have a conservative cap"); + assertTrue(count > -1, "RemoveFriendEvent must read the client supplied count"); + assertTrue(guard > count, "RemoveFriendEvent must validate the count after reading it"); + assertTrue(guard < loop, "RemoveFriendEvent must validate the count before looping"); + assertTrue(idGuard > loop && idGuard < unfriend, + "RemoveFriendEvent must skip invalid ids before mutating friendships"); + } + + @Test + void acceptFriendRequestsBoundsClientSuppliedBatchCountBeforeLoadingTargets() throws Exception { + String source = source("AcceptFriendRequestEvent"); + + int count = source.indexOf("int count = this.packet.readInt()"); + int guard = source.indexOf("count <= 0 || count > 100", count); + int loop = source.indexOf("for (int i = 0; i < count; i++)", count); + int idGuard = source.indexOf("userId <= 0", loop); + int loadTarget = source.indexOf("getHabbo(userId)", loop); + + assertTrue(count > -1, "AcceptFriendRequestEvent must read the client supplied count"); + assertTrue(guard > count && guard < loop, + "AcceptFriendRequestEvent must validate the count before looping"); + assertTrue(idGuard > loop && idGuard < loadTarget, + "AcceptFriendRequestEvent must skip invalid ids before loading targets"); + } + + @Test + void friendRequestAndMessagesUseSharedInputGuards() throws Exception { + String guard = source("FriendInputGuard"); + String request = source("FriendRequestEvent"); + String privateMessage = source("FriendPrivateMessageEvent"); + String invite = source("InviteFriendsEvent"); + + assertTrue(guard.contains("MAX_USERNAME_LENGTH = 15"), + "Friend request usernames should keep the Habbo username length bound"); + assertTrue(guard.contains("MAX_MESSAGE_LENGTH = 255"), + "Messenger payloads should keep the client message length bound"); + assertTrue(request.contains("FriendInputGuard.normalizeUsername"), + "Friend requests should normalize usernames before lookup"); + assertTrue(request.contains("FriendInputGuard.isValidUsername"), + "Friend requests should reject empty or oversized usernames before DB lookup"); + assertTrue(request.contains("Messenger.friendRequested(targetId, this.client.getHabbo().getHabboInfo().getId())"), + "Friend requests should reject duplicate outgoing requests"); + assertTrue(privateMessage.contains("FriendInputGuard.normalizeMessage"), + "Private messages should be normalized and capped before plugin dispatch"); + assertTrue(invite.contains("FriendInputGuard.normalizeMessage"), + "Room invites should be normalized and capped before fan-out"); + } + + @Test + void relationshipChangesFirePluginEventAndValidatePluginMutation() throws Exception { + String source = source("ChangeRelationEvent"); + + int event = source.indexOf("new UserRelationShipEvent"); + int fire = source.indexOf("Emulator.getPluginManager().fireEvent(event)", event); + int pluginGuard = source.indexOf("FriendInputGuard.isValidRelation(event.relationShip)", fire); + int setRelation = source.indexOf("buddy.setRelation(event.relationShip)", pluginGuard); + + assertTrue(source.contains("FriendInputGuard.isValidRelation(relationId)"), + "Relationship changes should reject invalid client relation ids"); + assertTrue(event > -1 && fire > event, + "Relationship changes should dispatch the plugin event before applying changes"); + assertTrue(pluginGuard > fire && pluginGuard < setRelation, + "Relationship changes should reject invalid plugin-mutated relation ids"); + } +} From 5b2c9f0aee5c41fb24ab31e0ab985023427f4555 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 20:47:58 +0200 Subject: [PATCH 12/18] fix(users): bound profile setting inputs --- .../users/RequestProfileFriendsEvent.java | 4 ++ .../users/RequestUserProfileEvent.java | 4 ++ .../users/RequestWearingBadgesEvent.java | 4 ++ .../incoming/users/SaveMottoEvent.java | 15 +++-- .../incoming/users/SaveUserVolumesEvent.java | 6 +- .../incoming/users/UpdateUIFlagsEvent.java | 2 +- .../incoming/users/UserInputGuard.java | 26 ++++++++ .../users/UserInputGuardContractTest.java | 64 +++++++++++++++++++ 8 files changed, 114 insertions(+), 11 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/users/UserInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/users/UserInputGuardContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/RequestProfileFriendsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/RequestProfileFriendsEvent.java index cc6a0198..29f51dcf 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/RequestProfileFriendsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/RequestProfileFriendsEvent.java @@ -10,6 +10,10 @@ public class RequestProfileFriendsEvent extends MessageHandler { @Override public void handle() throws Exception { int userId = this.packet.readInt(); + + if (!UserInputGuard.isPositiveId(userId)) + return; + Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); if (habbo != null) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/RequestUserProfileEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/RequestUserProfileEvent.java index e95b7e70..80dd5f57 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/RequestUserProfileEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/RequestUserProfileEvent.java @@ -10,6 +10,10 @@ public class RequestUserProfileEvent extends MessageHandler { @Override public void handle() throws Exception { int habboId = this.packet.readInt(); + + if (!UserInputGuard.isPositiveId(habboId)) + return; + Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(habboId); if (habbo != null) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/RequestWearingBadgesEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/RequestWearingBadgesEvent.java index ade93a20..bd0a0ba2 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/RequestWearingBadgesEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/RequestWearingBadgesEvent.java @@ -10,6 +10,10 @@ public class RequestWearingBadgesEvent extends MessageHandler { @Override public void handle() throws Exception { int userId = this.packet.readInt(); + + if (!UserInputGuard.isPositiveId(userId)) + return; + Habbo habbo = Emulator.getGameServer().getGameClientManager().getHabbo(userId); if (habbo == null || habbo.getHabboInfo() == null || habbo.getInventory() == null || habbo.getInventory().getBadgesComponent() == null) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/SaveMottoEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/SaveMottoEvent.java index d3f4322f..9074f2e8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/SaveMottoEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/SaveMottoEvent.java @@ -9,15 +9,16 @@ import com.eu.habbo.plugin.events.users.UserSavedMottoEvent; public class SaveMottoEvent extends MessageHandler { @Override public void handle() throws Exception { - String motto = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo()); + String motto = UserInputGuard.normalizeText(Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo())); UserSavedMottoEvent event = new UserSavedMottoEvent(this.client.getHabbo(), this.client.getHabbo().getHabboInfo().getMotto(), motto); Emulator.getPluginManager().fireEvent(event); - motto = event.newMotto; - - if(motto.length() <= Emulator.getConfig().getInt("motto.max_length", 38)) { - this.client.getHabbo().getHabboInfo().setMotto(motto); - this.client.getHabbo().getHabboInfo().run(); - } + motto = UserInputGuard.normalizeText(event.newMotto); + + if (motto.length() > Emulator.getConfig().getInt("motto.max_length", 38)) + return; + + this.client.getHabbo().getHabboInfo().setMotto(motto); + this.client.getHabbo().getHabboInfo().run(); if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) { this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(this.client.getHabbo()).compose()); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/SaveUserVolumesEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/SaveUserVolumesEvent.java index ff50cb9d..66d83eaa 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/SaveUserVolumesEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/SaveUserVolumesEvent.java @@ -7,9 +7,9 @@ import com.eu.habbo.plugin.events.users.UserSavedSettingsEvent; public class SaveUserVolumesEvent extends MessageHandler { @Override public void handle() throws Exception { - int system = this.packet.readInt(); - int furni = this.packet.readInt(); - int trax = this.packet.readInt(); + int system = UserInputGuard.clampVolume(this.packet.readInt()); + int furni = UserInputGuard.clampVolume(this.packet.readInt()); + int trax = UserInputGuard.clampVolume(this.packet.readInt()); this.client.getHabbo().getHabboStats().volumeSystem = system; this.client.getHabbo().getHabboStats().volumeFurni = furni; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/UpdateUIFlagsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/UpdateUIFlagsEvent.java index 8983b29e..79b37475 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/UpdateUIFlagsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/UpdateUIFlagsEvent.java @@ -5,7 +5,7 @@ import com.eu.habbo.messages.incoming.MessageHandler; public class UpdateUIFlagsEvent extends MessageHandler { @Override public void handle() throws Exception { - int flags = this.packet.readInt(); + int flags = UserInputGuard.sanitizeUiFlags(this.packet.readInt()); this.client.getHabbo().getHabboStats().uiFlags = flags; this.client.getHabbo().getHabboStats().run(); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/UserInputGuard.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/UserInputGuard.java new file mode 100644 index 00000000..2c2857fa --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/users/UserInputGuard.java @@ -0,0 +1,26 @@ +package com.eu.habbo.messages.incoming.users; + +final class UserInputGuard { + static final int MIN_VOLUME = 0; + static final int MAX_VOLUME = 100; + static final int MAX_UI_FLAGS = 0xFFFF; + + private UserInputGuard() { + } + + static boolean isPositiveId(int id) { + return id > 0; + } + + static int clampVolume(int volume) { + return Math.max(MIN_VOLUME, Math.min(MAX_VOLUME, volume)); + } + + static int sanitizeUiFlags(int flags) { + return flags < 0 ? 0 : flags & MAX_UI_FLAGS; + } + + static String normalizeText(String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/users/UserInputGuardContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/users/UserInputGuardContractTest.java new file mode 100644 index 00000000..6f44a158 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/users/UserInputGuardContractTest.java @@ -0,0 +1,64 @@ +package com.eu.habbo.messages.incoming.users; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class UserInputGuardContractTest { + private static String source(String name) throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/users/" + name + ".java")); + } + + @Test + void userProfileLookupsRejectInvalidIdsBeforeOfflineQueries() throws Exception { + for (String handler : new String[]{"RequestUserProfileEvent", "RequestProfileFriendsEvent", "RequestWearingBadgesEvent"}) { + String source = source(handler); + int read = source.indexOf("this.packet.readInt()"); + int guard = source.indexOf("UserInputGuard.isPositiveId", read); + int lookup = Math.max(source.indexOf("getOfflineHabboInfo", guard), source.indexOf("getBadgesOfflineHabbo", guard)); + + assertTrue(guard > read, handler + " should validate the packet id after reading it"); + assertTrue(lookup == -1 || guard < lookup, handler + " should validate ids before offline lookups"); + } + } + + @Test + void settingsInputsAreNormalizedBeforePersistence() throws Exception { + String volumes = source("SaveUserVolumesEvent"); + String flags = source("UpdateUIFlagsEvent"); + + assertTrue(volumes.contains("UserInputGuard.clampVolume(this.packet.readInt())"), + "volume settings should be clamped to the client-supported range"); + assertTrue(flags.contains("UserInputGuard.sanitizeUiFlags(this.packet.readInt())"), + "UI flags should be sanitized before persistence"); + } + + @Test + void mottoIsNormalizedAndRejectedBeforeSaveSideEffects() throws Exception { + String motto = source("SaveMottoEvent"); + + int pluginValue = motto.indexOf("UserInputGuard.normalizeText(event.newMotto)"); + int lengthGuard = motto.indexOf("motto.length() > Emulator.getConfig().getInt", pluginValue); + int save = motto.indexOf("setMotto(motto)", lengthGuard); + int achievement = motto.indexOf("AchievementManager.progressAchievement", save); + + assertTrue(pluginValue > -1, "plugin-mutated motto should be normalized"); + assertTrue(lengthGuard > pluginValue && lengthGuard < save, + "motto length should be validated before saving"); + assertTrue(save < achievement, + "motto achievement should only progress after a valid save"); + } + + @Test + void helperClampsAndMasksValues() { + assertEquals(0, UserInputGuard.clampVolume(-20)); + assertEquals(40, UserInputGuard.clampVolume(40)); + assertEquals(100, UserInputGuard.clampVolume(101)); + assertEquals(0, UserInputGuard.sanitizeUiFlags(-1)); + assertEquals(0xFFFF, UserInputGuard.sanitizeUiFlags(0x1FFFF)); + } +} From 3342b22a76d425e5660360cea214f297d9984a69 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 20:57:11 +0200 Subject: [PATCH 13/18] fix(rooms): bound room user inputs --- .../rooms/users/RoomUserActionEvent.java | 3 + .../rooms/users/RoomUserBanEvent.java | 4 + .../rooms/users/RoomUserGiveRightsEvent.java | 4 + .../rooms/users/RoomUserInputGuard.java | 17 ++++ .../rooms/users/RoomUserKickEvent.java | 15 +-- .../rooms/users/RoomUserMuteEvent.java | 4 + .../users/RoomUserRemoveRightsEvent.java | 3 + .../rooms/users/UnbanRoomUserEvent.java | 4 + .../users/RoomUserInputGuardContractTest.java | 91 +++++++++++++++++++ 9 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserInputGuardContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserActionEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserActionEvent.java index bc32465b..6607652e 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserActionEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserActionEvent.java @@ -28,6 +28,9 @@ public class RoomUserActionEvent extends MessageHandler { } int action = this.packet.readInt(); + if (!RoomUserInputGuard.isValidAction(action)) + return; + int wiredAction = 0; if (action == 5) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserBanEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserBanEvent.java index df071e0a..b999c87d 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserBanEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserBanEvent.java @@ -12,6 +12,10 @@ public class RoomUserBanEvent extends MessageHandler { int roomId = this.packet.readInt(); String banName = this.packet.readString(); + if (!RoomUserInputGuard.isPositiveId(userId) || !RoomUserInputGuard.isPositiveId(roomId)) { + return; + } + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room == null || room.getId() != roomId) { return; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserGiveRightsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserGiveRightsEvent.java index 479b0612..8d1ed63a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserGiveRightsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserGiveRightsEvent.java @@ -13,6 +13,10 @@ public class RoomUserGiveRightsEvent extends MessageHandler { public void handle() throws Exception { int userId = this.packet.readInt(); + if (!RoomUserInputGuard.isPositiveId(userId)) { + return; + } + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room == null) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserInputGuard.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserInputGuard.java new file mode 100644 index 00000000..317e0897 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserInputGuard.java @@ -0,0 +1,17 @@ +package com.eu.habbo.messages.incoming.rooms.users; + +final class RoomUserInputGuard { + static final int MIN_ACTION_ID = 0; + static final int MAX_ACTION_ID = 7; + + private RoomUserInputGuard() { + } + + static boolean isPositiveId(int id) { + return id > 0; + } + + static boolean isValidAction(int action) { + return action >= MIN_ACTION_ID && action <= MAX_ACTION_ID; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserKickEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserKickEvent.java index 814b0599..27e6bc01 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserKickEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserKickEvent.java @@ -21,6 +21,9 @@ public class RoomUserKickEvent extends MessageHandler { int userId = this.packet.readInt(); + if (!RoomUserInputGuard.isPositiveId(userId)) + return; + Habbo target = room.getHabbo(userId); if (target == null) @@ -35,15 +38,15 @@ public class RoomUserKickEvent extends MessageHandler { return; } - UserKickEvent event = new UserKickEvent(this.client.getHabbo(), target); - Emulator.getPluginManager().fireEvent(event); - - if (event.isCancelled()) - return; - if (room.hasRights(this.client.getHabbo()) || this.client.getHabbo().hasPermission(Permission.ACC_ANYROOMOWNER) || this.client.getHabbo().hasPermission(Permission.ACC_AMBASSADOR)) { if (target.hasPermission(Permission.ACC_UNKICKABLE)) return; + UserKickEvent event = new UserKickEvent(this.client.getHabbo(), target); + Emulator.getPluginManager().fireEvent(event); + + if (event.isCancelled()) + return; + room.kickHabbo(target, true); AchievementManager.progressAchievement(this.client.getHabbo(), Emulator.getGameEnvironment().getAchievementManager().getAchievement("SelfModKickSeen")); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserMuteEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserMuteEvent.java index 7a8a55bf..9cc6c373 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserMuteEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserMuteEvent.java @@ -18,6 +18,10 @@ public class RoomUserMuteEvent extends MessageHandler { int roomId = this.packet.readInt(); int minutes = this.packet.readInt(); + if (!RoomUserInputGuard.isPositiveId(userId) || !RoomUserInputGuard.isPositiveId(roomId)) { + return; + } + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room == null || room.getId() != roomId) { return; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserRemoveRightsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserRemoveRightsEvent.java index 987ec101..f6041563 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserRemoveRightsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserRemoveRightsEvent.java @@ -27,6 +27,9 @@ public class RoomUserRemoveRightsEvent extends MessageHandler { for (int i = 0; i < amount; i++) { int userId = this.packet.readInt(); + if (!RoomUserInputGuard.isPositiveId(userId)) + continue; + room.removeRights(userId); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/UnbanRoomUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/UnbanRoomUserEvent.java index 662d8d30..369f5bfb 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/UnbanRoomUserEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/UnbanRoomUserEvent.java @@ -10,6 +10,10 @@ public class UnbanRoomUserEvent extends MessageHandler { int userId = this.packet.readInt(); int roomId = this.packet.readInt(); + if (!RoomUserInputGuard.isPositiveId(userId) || !RoomUserInputGuard.isPositiveId(roomId)) { + return; + } + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room == null || room.getId() != roomId) { return; diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserInputGuardContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserInputGuardContractTest.java new file mode 100644 index 00000000..3f319b88 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserInputGuardContractTest.java @@ -0,0 +1,91 @@ +package com.eu.habbo.messages.incoming.rooms.users; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RoomUserInputGuardContractTest { + private static String source(String name) throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/users/" + name + ".java")); + } + + @Test + void roomModerationHandlersRejectInvalidUserAndRoomIds() throws Exception { + for (String handler : new String[]{"RoomUserBanEvent", "UnbanRoomUserEvent", "RoomUserMuteEvent"}) { + String source = source(handler); + int userRead = source.indexOf("int userId = this.packet.readInt()"); + int roomRead = source.indexOf("int roomId = this.packet.readInt()", userRead); + int guard = source.indexOf("RoomUserInputGuard.isPositiveId(userId)", roomRead); + int roomLookup = source.indexOf("getCurrentRoom()", guard); + + assertTrue(userRead > -1 && roomRead > userRead, handler + " should read user and room ids"); + assertTrue(guard > roomRead && guard < roomLookup, + handler + " should reject invalid ids before resolving room state"); + } + } + + @Test + void rightsMutationHandlersRejectInvalidUserIds() throws Exception { + String giveRights = source("RoomUserGiveRightsEvent"); + String removeRights = source("RoomUserRemoveRightsEvent"); + + int giveRead = giveRights.indexOf("int userId = this.packet.readInt()"); + int giveGuard = giveRights.indexOf("RoomUserInputGuard.isPositiveId(userId)", giveRead); + int giveTarget = giveRights.indexOf("room.getHabbo(userId)", giveGuard); + + int removeRead = removeRights.indexOf("int userId = this.packet.readInt()"); + int removeGuard = removeRights.indexOf("RoomUserInputGuard.isPositiveId(userId)", removeRead); + int removeCall = removeRights.indexOf("room.removeRights(userId)", removeGuard); + + assertTrue(giveGuard > giveRead && giveGuard < giveTarget, + "give-rights should validate target id before online/friend lookups"); + assertTrue(removeGuard > removeRead && removeGuard < removeCall, + "remove-rights should skip invalid ids before removing rights"); + } + + @Test + void kickPluginEventOnlyFiresAfterPermissionCheck() throws Exception { + String source = source("RoomUserKickEvent"); + + int userRead = source.indexOf("int userId = this.packet.readInt()"); + int idGuard = source.indexOf("RoomUserInputGuard.isPositiveId(userId)", userRead); + int targetLookup = source.indexOf("room.getHabbo(userId)", idGuard); + int permissionCheck = source.indexOf("room.hasRights(this.client.getHabbo())", targetLookup); + int event = source.indexOf("new UserKickEvent", permissionCheck); + int kick = source.indexOf("room.kickHabbo(target, true)", event); + + assertTrue(idGuard > userRead && idGuard < targetLookup, + "kick should validate target id before room lookup"); + assertTrue(permissionCheck > targetLookup && event > permissionCheck && event < kick, + "kick plugin event should only fire once the actor is authorized"); + } + + @Test + void roomActionsRejectUnknownActionIdsBeforeComposersAndWired() throws Exception { + String source = source("RoomUserActionEvent"); + + int actionRead = source.indexOf("int action = this.packet.readInt()"); + int guard = source.indexOf("RoomUserInputGuard.isValidAction(action)", actionRead); + int composer = source.indexOf("new RoomUserActionComposer", guard); + int wired = source.indexOf("WiredManager.triggerUserPerformsAction", guard); + + assertTrue(guard > actionRead && guard < composer, + "room actions should reject unknown ids before composing room state"); + assertTrue(guard < wired, + "room actions should reject unknown ids before wired triggers"); + } + + @Test + void helperBoundsExpectedRanges() { + assertFalse(RoomUserInputGuard.isPositiveId(0)); + assertTrue(RoomUserInputGuard.isPositiveId(1)); + assertFalse(RoomUserInputGuard.isValidAction(-1)); + assertTrue(RoomUserInputGuard.isValidAction(0)); + assertTrue(RoomUserInputGuard.isValidAction(7)); + assertFalse(RoomUserInputGuard.isValidAction(8)); + } +} From ba80870df0e28495aa89b2053d251b7c5a8ec155 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 21:01:00 +0200 Subject: [PATCH 14/18] fix(rooms): bound room item inputs --- .../rooms/items/AdvertisingSaveEvent.java | 16 +- .../items/FootballGateSaveLookEvent.java | 14 +- .../rooms/items/MannequinSaveLookEvent.java | 6 +- .../rooms/items/MannequinSaveNameEvent.java | 8 +- .../items/MoodLightSaveSettingsEvent.java | 3 + .../rooms/items/MoveWallItemEvent.java | 2 +- .../rooms/items/PostItDeleteEvent.java | 3 + .../rooms/items/PostItPlaceEvent.java | 3 + .../rooms/items/PostItSaveDataEvent.java | 3 + .../rooms/items/RedeemClothingEvent.java | 7 + .../incoming/rooms/items/RedeemItemEvent.java | 8 +- .../rooms/items/RoomItemInputGuard.java | 51 ++++++ .../rooms/items/RoomPickupItemEvent.java | 3 + .../rooms/items/RoomPlaceItemEvent.java | 22 ++- .../rooms/items/RotateMoveItemEvent.java | 4 +- .../items/SavePostItStickyPoleEvent.java | 10 +- .../rooms/items/ToggleFloorItemEvent.java | 3 + .../rooms/items/ToggleWallItemEvent.java | 3 + .../items/UpdateFurniturePositionEvent.java | 2 + .../jukebox/JukeBoxAddSoundTrackEvent.java | 12 +- .../jukebox/JukeBoxRemoveSoundTrackEvent.java | 11 +- .../jukebox/JukeBoxRequestPlayListEvent.java | 9 +- .../lovelock/LoveLockStartConfirmEvent.java | 4 + .../rentablespace/RentSpaceCancelEvent.java | 4 + .../items/rentablespace/RentSpaceEvent.java | 4 + .../youtube/YoutubeRequestPlaylistChange.java | 13 +- .../youtube/YoutubeRequestPlaylists.java | 6 +- .../youtube/YoutubeRequestStateChange.java | 6 +- .../items/RoomItemInputGuardContractTest.java | 154 ++++++++++++++++++ 29 files changed, 362 insertions(+), 32 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomItemInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/items/RoomItemInputGuardContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/AdvertisingSaveEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/AdvertisingSaveEvent.java index 7427d651..c70a0ce6 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/AdvertisingSaveEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/AdvertisingSaveEvent.java @@ -18,7 +18,11 @@ public class AdvertisingSaveEvent extends MessageHandler { if (!room.hasRights(this.client.getHabbo())) return; - HabboItem item = room.getHabboItem(this.packet.readInt()); + int itemId = this.packet.readInt(); + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + + HabboItem item = room.getHabboItem(itemId); if (item == null) return; @@ -29,9 +33,15 @@ public class AdvertisingSaveEvent extends MessageHandler { if (item instanceof InteractionCustomValues) { THashMap oldValues = new THashMap<>(((InteractionCustomValues) item).values); int count = this.packet.readInt(); + if (!RoomItemInputGuard.isValidCustomValueCount(count)) + return; + for (int i = 0; i < count / 2; i++) { - String key = this.packet.readString(); - String value = this.packet.readString(); + String key = RoomItemInputGuard.trimToMax(this.packet.readString(), RoomItemInputGuard.MAX_CUSTOM_KEY_LENGTH); + String value = RoomItemInputGuard.trimToMax(this.packet.readString(), RoomItemInputGuard.MAX_CUSTOM_VALUE_LENGTH); + + if (key.isEmpty()) + continue; if (!Emulator.getConfig().getBoolean("camera.use.https")) { value = value.replace("https://", "http://"); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/FootballGateSaveLookEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/FootballGateSaveLookEvent.java index aa4d2f30..bb1eefd2 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/FootballGateSaveLookEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/FootballGateSaveLookEvent.java @@ -13,15 +13,21 @@ public class FootballGateSaveLookEvent extends MessageHandler { if (room == null || this.client.getHabbo().getHabboInfo().getId() != room.getOwnerId()) return; - HabboItem item = room.getHabboItem(this.packet.readInt()); + int itemId = this.packet.readInt(); + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + + HabboItem item = room.getHabboItem(itemId); if (!(item instanceof InteractionFootballGate)) return; String gender = this.packet.readString(); - String look = this.packet.readString(); + String look = RoomItemInputGuard.trimToMax(this.packet.readString(), RoomItemInputGuard.MAX_LOOK_LENGTH); + + if (!RoomItemInputGuard.isValidGender(gender) || look.isEmpty()) + return; switch (gender.toLowerCase()) { - default: case "m": ((InteractionFootballGate) item).setFigureM(look); room.updateItem(item); @@ -33,4 +39,4 @@ public class FootballGateSaveLookEvent extends MessageHandler { break; } } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/MannequinSaveLookEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/MannequinSaveLookEvent.java index c62c7ad4..2e37eb43 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/MannequinSaveLookEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/MannequinSaveLookEvent.java @@ -15,7 +15,11 @@ public class MannequinSaveLookEvent extends MessageHandler { if (room == null || !room.isOwner(habbo)) return; - HabboItem item = room.getHabboItem(this.packet.readInt()); + int itemId = this.packet.readInt(); + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + + HabboItem item = room.getHabboItem(itemId); if (item == null) return; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/MannequinSaveNameEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/MannequinSaveNameEvent.java index c5c94204..61fcd1aa 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/MannequinSaveNameEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/MannequinSaveNameEvent.java @@ -12,12 +12,16 @@ public class MannequinSaveNameEvent extends MessageHandler { if (room == null || !room.isOwner(this.client.getHabbo())) return; - HabboItem item = room.getHabboItem(this.packet.readInt()); + int itemId = this.packet.readInt(); + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + + HabboItem item = room.getHabboItem(itemId); if (item == null) return; String[] data = item.getExtradata().split(":"); - String name = this.packet.readString(); + String name = RoomItemInputGuard.trimToMax(this.packet.readString(), 32); if (name.length() < 3 || name.length() > 15) { name = Emulator.getTexts().getValue("hotel.mannequin.name.default", "My look"); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/MoodLightSaveSettingsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/MoodLightSaveSettingsEvent.java index 894e83c9..88e6f3aa 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/MoodLightSaveSettingsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/MoodLightSaveSettingsEvent.java @@ -21,6 +21,9 @@ public class MoodLightSaveSettingsEvent extends MessageHandler { public void handle() throws Exception { Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); + if (room == null) + return; + if ((room.getGuildId() <= 0 && room.getGuildRightLevel(this.client.getHabbo()).isLessThan(RoomRightLevels.GUILD_RIGHTS)) && !room.hasRights(this.client.getHabbo())) return; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/MoveWallItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/MoveWallItemEvent.java index 10397073..075d4620 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/MoveWallItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/MoveWallItemEvent.java @@ -25,7 +25,7 @@ public class MoveWallItemEvent extends MessageHandler { int itemId = this.packet.readInt(); String wallPosition = this.packet.readString(); - if (itemId <= 0 || wallPosition.length() <= 13) + if (!RoomItemInputGuard.isPositiveId(itemId) || wallPosition.length() <= 13) return; HabboItem item = room.getHabboItem(itemId); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/PostItDeleteEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/PostItDeleteEvent.java index 821f92c8..a65cd8a8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/PostItDeleteEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/PostItDeleteEvent.java @@ -15,6 +15,9 @@ public class PostItDeleteEvent extends MessageHandler { public void handle() throws Exception { int itemId = this.packet.readInt(); + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room == null) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/PostItPlaceEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/PostItPlaceEvent.java index 67f1d202..f8a006a4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/PostItPlaceEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/PostItPlaceEvent.java @@ -19,6 +19,9 @@ public class PostItPlaceEvent extends MessageHandler { int itemId = this.packet.readInt(); String location = this.packet.readString(); + if (!RoomItemInputGuard.isPositiveId(itemId) || location.length() <= 13) + return; + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room != null) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/PostItSaveDataEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/PostItSaveDataEvent.java index 24f2cbf7..0752e14a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/PostItSaveDataEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/PostItSaveDataEvent.java @@ -20,6 +20,9 @@ public class PostItSaveDataEvent extends MessageHandler { String color = this.packet.readString(); String text = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString().replace(((char) 9) + "", ""), this.client.getHabbo()); + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + if (text.length() > Emulator.getConfig().getInt("postit.charlimit")) { ScripterManager.scripterDetected(this.client, Emulator.getTexts().getValue("scripter.warning.sticky.size").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%amount%", text.length() + "").replace("%limit%", Emulator.getConfig().getInt("postit.charlimit") + "")); return; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RedeemClothingEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RedeemClothingEvent.java index 2ab95a0d..5283e1e4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RedeemClothingEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RedeemClothingEvent.java @@ -26,6 +26,9 @@ public class RedeemClothingEvent extends MessageHandler { public void handle() throws Exception { int itemId = this.packet.readInt(); + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null && this.client.getHabbo().getHabboInfo().getCurrentRoom().hasRights(this.client.getHabbo())) { HabboItem item = this.client.getHabbo().getHabboInfo().getCurrentRoom().getHabboItem(itemId); @@ -42,6 +45,10 @@ public class RedeemClothingEvent extends MessageHandler { item.setRoomId(0); RoomTile tile = this.client.getHabbo().getHabboInfo().getCurrentRoom().getLayout().getTile(item.getX(), item.getY()); + if (tile == null) { + return; + } + this.client.getHabbo().getHabboInfo().getCurrentRoom().removeHabboItem(item); this.client.getHabbo().getHabboInfo().getCurrentRoom().updateTile(tile); this.client.getHabbo().getHabboInfo().getCurrentRoom().sendComposer(new UpdateStackHeightComposer(tile.x, tile.y, tile.z, tile.relativeHeight()).compose()); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RedeemItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RedeemItemEvent.java index 929e7a95..c5a15521 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RedeemItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RedeemItemEvent.java @@ -19,6 +19,9 @@ public class RedeemItemEvent extends MessageHandler { public void handle() throws Exception { int itemId = this.packet.readInt(); + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room != null) { @@ -98,6 +101,9 @@ public class RedeemItemEvent extends MessageHandler { room.removeHabboItem(item); room.sendComposer(new RemoveFloorItemComposer(item).compose()); RoomTile t = room.getLayout().getTile(item.getX(), item.getY()); + if (t == null) + return; + t.setStackHeight(room.getStackHeight(item.getX(), item.getY(), false)); room.updateTile(t); room.sendComposer(new UpdateStackHeightComposer(item.getX(), item.getY(), t.z, t.relativeHeight()).compose()); @@ -124,4 +130,4 @@ public class RedeemItemEvent extends MessageHandler { } } } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomItemInputGuard.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomItemInputGuard.java new file mode 100644 index 00000000..9172330b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomItemInputGuard.java @@ -0,0 +1,51 @@ +package com.eu.habbo.messages.incoming.rooms.items; + +public final class RoomItemInputGuard { + public static final int MAX_CUSTOM_VALUE_PAIRS = 20; + public static final int MAX_CUSTOM_KEY_LENGTH = 64; + public static final int MAX_CUSTOM_VALUE_LENGTH = 512; + public static final int MAX_LOOK_LENGTH = 512; + public static final int MAX_YOUTUBE_PLAYLIST_ID_LENGTH = 128; + public static final int MAX_STICKY_POLE_COMMANDS = 10; + public static final int MAX_STICKY_POLE_COMMAND_LENGTH = 255; + + private RoomItemInputGuard() { + } + + public static boolean isPositiveId(int id) { + return id > 0; + } + + public static boolean isValidCustomValueCount(int count) { + return count > 0 && count % 2 == 0 && count / 2 <= MAX_CUSTOM_VALUE_PAIRS; + } + + public static String trimToMax(String value, int maxLength) { + if (value == null) { + return ""; + } + + String trimmed = value.trim(); + return trimmed.length() > maxLength ? trimmed.substring(0, maxLength) : trimmed; + } + + public static boolean isValidGender(String gender) { + return "m".equalsIgnoreCase(gender) || "f".equalsIgnoreCase(gender); + } + + public static Integer parseInt(String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return null; + } + } + + public static Short parseShort(String value) { + try { + return Short.parseShort(value); + } catch (NumberFormatException e) { + return null; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPickupItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPickupItemEvent.java index b1797f8a..6241510c 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPickupItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPickupItemEvent.java @@ -12,6 +12,9 @@ public class RoomPickupItemEvent extends MessageHandler { this.packet.readInt(); //10 = floorItem and 20 = wallItem int itemId = this.packet.readInt(); + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room == null) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java index 1b23813b..1c535783 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RoomPlaceItemEvent.java @@ -18,9 +18,12 @@ public class RoomPlaceItemEvent extends MessageHandler { public void handle() throws Exception { String[] values = this.packet.readString().split(" "); - int itemId = -1; + if (values.length == 0) + return; - if (values.length != 0) itemId = Integer.parseInt(values[0]); + Integer itemId = RoomItemInputGuard.parseInt(values[0]); + if (itemId == null || !RoomItemInputGuard.isPositiveId(itemId)) + return; if (!this.client.getHabbo().getRoomUnit().isInRoom()) { this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, FurnitureMovementError.NO_RIGHTS.errorCode)); @@ -56,9 +59,15 @@ public class RoomPlaceItemEvent extends MessageHandler { } if (item.getBaseItem().getType() == FurnitureType.FLOOR) { - short x = Short.parseShort(values[1]); - short y = Short.parseShort(values[2]); - int rotation = Integer.parseInt(values[3]); + if (values.length < 4) + return; + + Short x = RoomItemInputGuard.parseShort(values[1]); + Short y = RoomItemInputGuard.parseShort(values[2]); + Integer rotation = RoomItemInputGuard.parseInt(values[3]); + + if (x == null || y == null || rotation == null) + return; RoomTile tile = room.getLayout().getTile(x, y); @@ -108,6 +117,9 @@ public class RoomPlaceItemEvent extends MessageHandler { return; } } else { + if (values.length < 4) + return; + FurnitureMovementError error = room.placeWallFurniAt(item, values[1] + " " + values[2] + " " + values[3], this.client.getHabbo()); if (!error.equals(FurnitureMovementError.NONE)) { this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, error.errorCode)); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RotateMoveItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RotateMoveItemEvent.java index 812fb6b2..895c0a5d 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RotateMoveItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/RotateMoveItemEvent.java @@ -16,6 +16,8 @@ public class RotateMoveItemEvent extends MessageHandler { if (room == null) return; int furniId = this.packet.readInt(); + if (!RoomItemInputGuard.isPositiveId(furniId)) return; + HabboItem item = room.getHabboItem(furniId); if (item == null) return; @@ -39,4 +41,4 @@ public class RotateMoveItemEvent extends MessageHandler { this.client.sendResponse(new FloorItemUpdateComposer(item)); } } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/SavePostItStickyPoleEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/SavePostItStickyPoleEvent.java index 9063b816..d1a7eb90 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/SavePostItStickyPoleEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/SavePostItStickyPoleEvent.java @@ -24,9 +24,14 @@ public class SavePostItStickyPoleEvent extends MessageHandler { if (itemId == -1234) { if (this.client.getHabbo().hasPermission("cmd_multi")) { String[] commands = this.packet.readString().split("\r"); + if (commands.length > RoomItemInputGuard.MAX_STICKY_POLE_COMMANDS) + return; for (String command : commands) { - command = command.replace("
", "\r"); + command = RoomItemInputGuard.trimToMax(command.replace("
", "\r"), RoomItemInputGuard.MAX_STICKY_POLE_COMMAND_LENGTH); + if (command.isEmpty()) + continue; + CommandHandler.handleCommand(this.client, command); } } else { @@ -38,6 +43,9 @@ public class SavePostItStickyPoleEvent extends MessageHandler { if (text.length() > Emulator.getConfig().getInt("postit.charlimit")) return; + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room == null) return; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleFloorItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleFloorItemEvent.java index 8bf4942b..d5706027 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleFloorItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleFloorItemEvent.java @@ -41,6 +41,9 @@ public class ToggleFloorItemEvent extends MessageHandler { int itemId = this.packet.readInt(); int state = this.packet.readInt(); + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + HabboItem item = room.getHabboItem(itemId); if (item == null || item instanceof InteractionDice) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleWallItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleWallItemEvent.java index c620900b..c54865be 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleWallItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/ToggleWallItemEvent.java @@ -24,6 +24,9 @@ public class ToggleWallItemEvent extends MessageHandler { int itemId = this.packet.readInt(); int state = this.packet.readInt(); + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + HabboItem item = room.getHabboItem(itemId); if (item == null) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/UpdateFurniturePositionEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/UpdateFurniturePositionEvent.java index f5bce826..bbaae2d8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/UpdateFurniturePositionEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/UpdateFurniturePositionEvent.java @@ -20,6 +20,8 @@ public class UpdateFurniturePositionEvent extends MessageHandler { if (room == null) return; int furniId = this.packet.readInt(); + if (!RoomItemInputGuard.isPositiveId(furniId)) return; + HabboItem item = room.getHabboItem(furniId); if (item == null) return; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxAddSoundTrackEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxAddSoundTrackEvent.java index 182d6862..79f6feb4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxAddSoundTrackEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxAddSoundTrackEvent.java @@ -1,26 +1,32 @@ package com.eu.habbo.messages.incoming.rooms.items.jukebox; import com.eu.habbo.habbohotel.items.interactions.InteractionMusicDisc; +import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.incoming.rooms.items.RoomItemInputGuard; public class JukeBoxAddSoundTrackEvent extends MessageHandler { @Override public void handle() throws Exception { - if (!this.client.getHabbo().getHabboInfo().getCurrentRoom().hasRights(this.client.getHabbo())) return; + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); + if (room == null || !room.hasRights(this.client.getHabbo())) return; int itemId = this.packet.readInt(); this.packet.readInt(); // slotId + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + Habbo habbo = this.client.getHabbo(); if (habbo != null) { HabboItem item = habbo.getInventory().getItemsComponent().getHabboItem(itemId); if (item instanceof InteractionMusicDisc && item.getRoomId() == 0) { - this.client.getHabbo().getHabboInfo().getCurrentRoom().getTraxManager().addSong((InteractionMusicDisc) item, habbo); + room.getTraxManager().addSong((InteractionMusicDisc) item, habbo); } } } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxRemoveSoundTrackEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxRemoveSoundTrackEvent.java index 26f79aff..03757c7e 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxRemoveSoundTrackEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxRemoveSoundTrackEvent.java @@ -1,6 +1,7 @@ package com.eu.habbo.messages.incoming.rooms.items.jukebox; import com.eu.habbo.habbohotel.items.interactions.InteractionMusicDisc; +import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.messages.incoming.MessageHandler; public class JukeBoxRemoveSoundTrackEvent extends MessageHandler { @@ -8,12 +9,16 @@ public class JukeBoxRemoveSoundTrackEvent extends MessageHandler { public void handle() throws Exception { int index = this.packet.readInt(); - if (this.client.getHabbo().getHabboInfo().getCurrentRoom() == null) return; + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); + if (room == null) return; - InteractionMusicDisc musicDisc = this.client.getHabbo().getHabboInfo().getCurrentRoom().getTraxManager().getSongs().get(index); + if (index < 0 || index >= room.getTraxManager().getSongs().size()) + return; + + InteractionMusicDisc musicDisc = room.getTraxManager().getSongs().get(index); if (musicDisc != null) { - this.client.getHabbo().getHabboInfo().getCurrentRoom().getTraxManager().removeSong(musicDisc.getId()); + room.getTraxManager().removeSong(musicDisc.getId()); } } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxRequestPlayListEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxRequestPlayListEvent.java index a5468d59..908b7135 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxRequestPlayListEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxRequestPlayListEvent.java @@ -1,6 +1,7 @@ package com.eu.habbo.messages.incoming.rooms.items.jukebox; import com.eu.habbo.habbohotel.rooms.TraxManager; +import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.rooms.items.jukebox.JukeBoxMySongsComposer; import com.eu.habbo.messages.outgoing.rooms.items.jukebox.JukeBoxPlayListComposer; @@ -8,9 +9,13 @@ import com.eu.habbo.messages.outgoing.rooms.items.jukebox.JukeBoxPlayListCompose public class JukeBoxRequestPlayListEvent extends MessageHandler { @Override public void handle() throws Exception { - TraxManager traxManager = this.client.getHabbo().getHabboInfo().getCurrentRoom().getTraxManager(); + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); + if (room == null) + return; + + TraxManager traxManager = room.getTraxManager(); this.client.sendResponse(new JukeBoxPlayListComposer(traxManager.getSongs(), traxManager.totalLength())); this.client.sendResponse(new JukeBoxMySongsComposer(traxManager.myList(this.client.getHabbo()))); - this.client.getHabbo().getHabboInfo().getCurrentRoom().getTraxManager().updateCurrentPlayingSong(this.client.getHabbo()); + traxManager.updateCurrentPlayingSong(this.client.getHabbo()); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/lovelock/LoveLockStartConfirmEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/lovelock/LoveLockStartConfirmEvent.java index d4588cdf..8ce2f7e8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/lovelock/LoveLockStartConfirmEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/lovelock/LoveLockStartConfirmEvent.java @@ -4,6 +4,7 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionLoveLock; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.incoming.rooms.items.RoomItemInputGuard; import com.eu.habbo.messages.outgoing.rooms.items.lovelock.LoveLockFurniFinishedComposer; import com.eu.habbo.messages.outgoing.rooms.items.lovelock.LoveLockFurniFriendConfirmedComposer; @@ -12,6 +13,9 @@ public class LoveLockStartConfirmEvent extends MessageHandler { public void handle() throws Exception { int itemId = this.packet.readInt(); + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + if (this.packet.readBoolean()) { if (this.client.getHabbo().getHabboInfo().getCurrentRoom() == null) return; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/rentablespace/RentSpaceCancelEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/rentablespace/RentSpaceCancelEvent.java index bc6bd3d2..d20b51c4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/rentablespace/RentSpaceCancelEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/rentablespace/RentSpaceCancelEvent.java @@ -5,12 +5,16 @@ import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.incoming.rooms.items.RoomItemInputGuard; public class RentSpaceCancelEvent extends MessageHandler { @Override public void handle() throws Exception { int itemId = this.packet.readInt(); + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room == null) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/rentablespace/RentSpaceEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/rentablespace/RentSpaceEvent.java index 1df3bd21..2ea0de14 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/rentablespace/RentSpaceEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/rentablespace/RentSpaceEvent.java @@ -4,12 +4,16 @@ import com.eu.habbo.habbohotel.items.interactions.InteractionRentableSpace; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.incoming.rooms.items.RoomItemInputGuard; public class RentSpaceEvent extends MessageHandler { @Override public void handle() throws Exception { int itemId = this.packet.readInt(); + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room == null) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylistChange.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylistChange.java index fbc5b7e1..7dab9b38 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylistChange.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylistChange.java @@ -8,6 +8,7 @@ import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.incoming.rooms.items.RoomItemInputGuard; import com.eu.habbo.messages.outgoing.rooms.items.youtube.YoutubeVideoComposer; import com.eu.habbo.threading.runnables.YoutubeAdvanceVideo; @@ -17,7 +18,10 @@ public class YoutubeRequestPlaylistChange extends MessageHandler { @Override public void handle() throws Exception { int itemId = this.packet.readInt(); - String playlistId = this.packet.readString(); + String playlistId = RoomItemInputGuard.trimToMax(this.packet.readString(), RoomItemInputGuard.MAX_YOUTUBE_PLAYLIST_ID_LENGTH); + + if (!RoomItemInputGuard.isPositiveId(itemId) || playlistId.isEmpty()) + return; Habbo habbo = this.client.getHabbo(); @@ -30,13 +34,16 @@ public class YoutubeRequestPlaylistChange extends MessageHandler { if (!room.isOwner(habbo) && !habbo.hasPermission(Permission.ACC_ANYROOMOWNER)) return; - HabboItem item = this.client.getHabbo().getHabboInfo().getCurrentRoom().getHabboItem(itemId); + HabboItem item = room.getHabboItem(itemId); if (item == null || !(item instanceof InteractionYoutubeTV)) return; Optional playlist = Emulator.getGameEnvironment().getItemManager().getYoutubeManager().getPlaylistsForItemId(item.getId()).stream().filter(p -> p.getId().equals(playlistId)).findAny(); if (playlist.isPresent()) { + if (playlist.get().getVideos().isEmpty()) + return; + YoutubeManager.YoutubeVideo video = playlist.get().getVideos().get(0); if (video == null) return; @@ -52,4 +59,4 @@ public class YoutubeRequestPlaylistChange extends MessageHandler { item.needsUpdate(true); } } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylists.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylists.java index 8b480bf5..3f95a8e3 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylists.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylists.java @@ -5,6 +5,7 @@ import com.eu.habbo.habbohotel.items.YoutubeManager; import com.eu.habbo.habbohotel.items.interactions.InteractionYoutubeTV; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.incoming.rooms.items.RoomItemInputGuard; import com.eu.habbo.messages.outgoing.handshake.ConnectionErrorComposer; import com.eu.habbo.messages.outgoing.rooms.items.youtube.YoutubeDisplayListComposer; import org.slf4j.Logger; @@ -19,6 +20,9 @@ public class YoutubeRequestPlaylists extends MessageHandler { public void handle() throws Exception { int itemId = this.packet.readInt(); + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + if (this.client.getHabbo().getHabboInfo().getCurrentRoom() != null) { HabboItem item = this.client.getHabbo().getHabboInfo().getCurrentRoom().getHabboItem(itemId); @@ -38,4 +42,4 @@ public class YoutubeRequestPlaylists extends MessageHandler { } } } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestStateChange.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestStateChange.java index fd3740a7..e634e198 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestStateChange.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestStateChange.java @@ -7,6 +7,7 @@ import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboItem; import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.incoming.rooms.items.RoomItemInputGuard; import com.eu.habbo.messages.outgoing.rooms.items.youtube.YoutubeStateChangeComposer; import com.eu.habbo.messages.outgoing.rooms.items.youtube.YoutubeVideoComposer; import com.eu.habbo.threading.runnables.YoutubeAdvanceVideo; @@ -49,6 +50,9 @@ public class YoutubeRequestStateChange extends MessageHandler { int itemId = this.packet.readInt(); YoutubeState state = YoutubeState.getByState(this.packet.readInt()); + if (!RoomItemInputGuard.isPositiveId(itemId)) + return; + if (state == null) return; Habbo habbo = this.client.getHabbo(); @@ -62,7 +66,7 @@ public class YoutubeRequestStateChange extends MessageHandler { if (!room.isOwner(habbo) && !habbo.hasPermission(Permission.ACC_ANYROOMOWNER)) return; - HabboItem item = this.client.getHabbo().getHabboInfo().getCurrentRoom().getHabboItem(itemId); + HabboItem item = room.getHabboItem(itemId); if (!(item instanceof InteractionYoutubeTV)) return; diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/items/RoomItemInputGuardContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/items/RoomItemInputGuardContractTest.java new file mode 100644 index 00000000..561a6543 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/rooms/items/RoomItemInputGuardContractTest.java @@ -0,0 +1,154 @@ +package com.eu.habbo.messages.incoming.rooms.items; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RoomItemInputGuardContractTest { + private static String source(String name) throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/" + name + ".java")); + } + + @Test + void itemMutationHandlersRejectInvalidIdsBeforeRoomLookups() throws Exception { + for (String handler : new String[]{"RoomPickupItemEvent", "RotateMoveItemEvent", "UpdateFurniturePositionEvent", "MoveWallItemEvent", "ToggleFloorItemEvent", "ToggleWallItemEvent", "AdvertisingSaveEvent", "MannequinSaveNameEvent", "MannequinSaveLookEvent", "FootballGateSaveLookEvent", "PostItSaveDataEvent", "PostItPlaceEvent", "PostItDeleteEvent"}) { + String source = source(handler); + int idRead = source.indexOf("this.packet.readInt()"); + int guard = source.indexOf("RoomItemInputGuard.isPositiveId", idRead); + int lookup = source.indexOf("getHabboItem", guard); + + assertTrue(guard > idRead, handler + " should validate item ids after reading them"); + assertTrue(lookup == -1 || guard < lookup, handler + " should validate item ids before room item lookups"); + } + } + + @Test + void specialItemHandlersRejectInvalidIdsBeforeLookups() throws Exception { + for (String handler : new String[]{ + "rentablespace/RentSpaceEvent", + "rentablespace/RentSpaceCancelEvent", + "lovelock/LoveLockStartConfirmEvent", + "youtube/YoutubeRequestPlaylistChange", + "youtube/YoutubeRequestPlaylists", + "youtube/YoutubeRequestStateChange", + "jukebox/JukeBoxAddSoundTrackEvent", + "RedeemItemEvent", + "RedeemClothingEvent" + }) { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/" + handler + ".java")); + int idRead = source.indexOf("this.packet.readInt()"); + int guard = source.indexOf("RoomItemInputGuard.isPositiveId", idRead); + + assertTrue(guard > idRead, handler + " should validate item ids after reading them"); + } + } + + @Test + void roomPlacementParsesClientPayloadSafely() throws Exception { + String source = source("RoomPlaceItemEvent"); + + assertTrue(source.contains("RoomItemInputGuard.parseInt(values[0])"), + "item placement should parse item id without throwing on malformed packets"); + assertTrue(source.contains("values.length < 4"), + "item placement should require complete coordinate payloads"); + assertTrue(source.contains("RoomItemInputGuard.parseShort(values[1])"), + "floor placement should parse x coordinate safely"); + assertTrue(source.contains("RoomItemInputGuard.parseShort(values[2])"), + "floor placement should parse y coordinate safely"); + assertTrue(source.contains("RoomItemInputGuard.parseInt(values[3])"), + "floor placement should parse rotation safely"); + } + + @Test + void advertisingCustomValuesAreBoundedBeforeMutation() throws Exception { + String source = source("AdvertisingSaveEvent"); + + int count = source.indexOf("int count = this.packet.readInt()"); + int guard = source.indexOf("RoomItemInputGuard.isValidCustomValueCount(count)", count); + int loop = source.indexOf("for (int i = 0; i < count / 2; i++)", guard); + int mutate = source.indexOf(".values.put(key, value)", loop); + + assertTrue(guard > count && guard < loop, + "custom value pair count should be bounded before reading key/value pairs"); + assertTrue(source.contains("RoomItemInputGuard.trimToMax(this.packet.readString(), RoomItemInputGuard.MAX_CUSTOM_KEY_LENGTH)"), + "custom value keys should be trimmed and capped"); + assertTrue(source.contains("RoomItemInputGuard.trimToMax(this.packet.readString(), RoomItemInputGuard.MAX_CUSTOM_VALUE_LENGTH)"), + "custom value values should be trimmed and capped"); + assertTrue(mutate > loop, + "custom values should only mutate after bounded reads"); + } + + @Test + void stickyPoleMultiCommandPayloadIsBounded() throws Exception { + String source = source("SavePostItStickyPoleEvent"); + + int split = source.indexOf("String[] commands = this.packet.readString().split"); + int countGuard = source.indexOf("commands.length > RoomItemInputGuard.MAX_STICKY_POLE_COMMANDS", split); + int trim = source.indexOf("RoomItemInputGuard.trimToMax(command.replace", countGuard); + int execute = source.indexOf("CommandHandler.handleCommand", trim); + + assertTrue(split > -1 && countGuard > split, + "sticky-pole multi-command packets should cap command count before looping"); + assertTrue(trim > countGuard && trim < execute, + "sticky-pole multi-command packets should cap each command before execution"); + } + + @Test + void specialLookPayloadsAreValidatedBeforeMutation() throws Exception { + String football = source("FootballGateSaveLookEvent"); + String mannequinName = source("MannequinSaveNameEvent"); + String moodlight = source("MoodLightSaveSettingsEvent"); + + assertTrue(football.contains("RoomItemInputGuard.isValidGender(gender)"), + "football gates should reject unknown gender keys instead of defaulting to male"); + assertTrue(football.contains("RoomItemInputGuard.trimToMax(this.packet.readString(), RoomItemInputGuard.MAX_LOOK_LENGTH)"), + "football gate looks should be capped before persistence"); + assertTrue(mannequinName.contains("RoomItemInputGuard.trimToMax(this.packet.readString(), 32)"), + "mannequin names should be capped before extradata persistence"); + assertTrue(moodlight.contains("if (room == null)"), + "moodlight saves should null-check current room before inspecting rights"); + } + + @Test + void youtubeAndJukeboxInputsAreBoundedBeforeListAccess() throws Exception { + String playlistChange = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/youtube/YoutubeRequestPlaylistChange.java")); + String jukeboxRemove = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxRemoveSoundTrackEvent.java")); + String jukeboxRequest = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/rooms/items/jukebox/JukeBoxRequestPlayListEvent.java")); + + int playlistTrim = playlistChange.indexOf("RoomItemInputGuard.trimToMax"); + int emptyVideos = playlistChange.indexOf("playlist.get().getVideos().isEmpty()"); + int getFirst = playlistChange.indexOf("playlist.get().getVideos().get(0)"); + + assertTrue(playlistTrim > -1, + "youtube playlist ids should be capped before lookup"); + assertTrue(emptyVideos > playlistTrim && emptyVideos < getFirst, + "youtube playlist changes should reject empty playlists before get(0)"); + assertTrue(jukeboxRemove.contains("index < 0 || index >= room.getTraxManager().getSongs().size()"), + "jukebox remove should bound client-provided indexes before list access"); + assertTrue(jukeboxRequest.contains("if (room == null)"), + "jukebox playlist requests should null-check current room"); + } + + @Test + void helperRejectsMalformedValues() { + assertFalse(RoomItemInputGuard.isPositiveId(0)); + assertTrue(RoomItemInputGuard.isPositiveId(1)); + assertFalse(RoomItemInputGuard.isValidCustomValueCount(0)); + assertFalse(RoomItemInputGuard.isValidCustomValueCount(3)); + assertTrue(RoomItemInputGuard.isValidCustomValueCount(RoomItemInputGuard.MAX_CUSTOM_VALUE_PAIRS * 2)); + assertFalse(RoomItemInputGuard.isValidCustomValueCount((RoomItemInputGuard.MAX_CUSTOM_VALUE_PAIRS + 1) * 2)); + assertEquals(123, RoomItemInputGuard.parseInt("123")); + assertNull(RoomItemInputGuard.parseInt("abc")); + assertEquals((short) 12, RoomItemInputGuard.parseShort("12")); + assertNull(RoomItemInputGuard.parseShort("40000")); + assertTrue(RoomItemInputGuard.isValidGender("m")); + assertTrue(RoomItemInputGuard.isValidGender("F")); + assertFalse(RoomItemInputGuard.isValidGender("x")); + } +} From e24020e9df1f74a9db80f7bf3ded8301e465349d Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 21:17:27 +0200 Subject: [PATCH 15/18] fix(modtool): bound staff supplied targets --- .../incoming/modtool/ModToolAlertEvent.java | 2 +- .../ModToolChangeRoomSettingsEvent.java | 8 ++++++- .../ModToolIssueDefaultSanctionEvent.java | 4 ++++ .../incoming/modtool/ModToolKickEvent.java | 2 +- .../modtool/ModToolRequestRoomInfoEvent.java | 4 ++++ .../ModToolRequestRoomVisitsEvent.java | 4 ++++ .../modtool/ModToolSanctionAlertEvent.java | 2 +- .../modtool/ModToolSanctionBanEvent.java | 2 +- .../modtool/ModToolSanctionMuteEvent.java | 2 +- .../ModToolSanctionTradeLockEvent.java | 2 +- .../incoming/modtool/ModToolWarnEvent.java | 2 +- .../ModToolPermissionContractTest.java | 24 +++++++++++++++++++ 12 files changed, 50 insertions(+), 8 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolAlertEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolAlertEvent.java index c2b6820f..75312562 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolAlertEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolAlertEvent.java @@ -14,7 +14,7 @@ public class ModToolAlertEvent extends MessageHandler { int userId = this.packet.readInt(); String message = ModToolInputGuard.normalize(this.packet.readString()); - if (!ModToolInputGuard.isSafeMessage(message)) { + if (!ModToolTicketGuard.isPositiveId(userId) || !ModToolInputGuard.isSafeMessage(message)) { return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolChangeRoomSettingsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolChangeRoomSettingsEvent.java index a84022df..48b8c5f6 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolChangeRoomSettingsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolChangeRoomSettingsEvent.java @@ -10,7 +10,13 @@ public class ModToolChangeRoomSettingsEvent extends MessageHandler { @Override public void handle() throws Exception { if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { - Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.packet.readInt()); + int roomId = this.packet.readInt(); + + if (!ModToolTicketGuard.isPositiveId(roomId)) { + return; + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId); if (room != null) { final boolean lockDoor = this.packet.readInt() == 1; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueDefaultSanctionEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueDefaultSanctionEvent.java index 2b0789f6..61e6c3ab 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueDefaultSanctionEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueDefaultSanctionEvent.java @@ -19,6 +19,10 @@ public class ModToolIssueDefaultSanctionEvent extends MessageHandler { this.packet.readInt(); int category = this.packet.readInt(); + if (!ModToolTicketGuard.isPositiveId(issueId) || !ModToolTicketGuard.isPositiveId(category)) { + return; + } + ModToolIssue issue = Emulator.getGameEnvironment().getModToolManager().getTicket(issueId); if (issue == null) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolKickEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolKickEvent.java index da01445a..c5ecb445 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolKickEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolKickEvent.java @@ -21,7 +21,7 @@ public class ModToolKickEvent extends MessageHandler { int userId = this.packet.readInt(); String message = ModToolInputGuard.normalize(this.packet.readString()); - if (!ModToolInputGuard.isSafeMessage(message)) { + if (!ModToolTicketGuard.isPositiveId(userId) || !ModToolInputGuard.isSafeMessage(message)) { return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestRoomInfoEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestRoomInfoEvent.java index 5f140550..c6cc5fb6 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestRoomInfoEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestRoomInfoEvent.java @@ -13,6 +13,10 @@ public class ModToolRequestRoomInfoEvent extends MessageHandler { if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { int roomId = this.packet.readInt(); + if (!ModToolTicketGuard.isPositiveId(roomId)) { + return; + } + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId); if (room != null) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestRoomVisitsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestRoomVisitsEvent.java index 57ba6578..b219be44 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestRoomVisitsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestRoomVisitsEvent.java @@ -12,6 +12,10 @@ public class ModToolRequestRoomVisitsEvent extends MessageHandler { if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { int userId = this.packet.readInt(); + if (!ModToolTicketGuard.isPositiveId(userId)) { + return; + } + HabboInfo habboInfo = Emulator.getGameEnvironment().getHabboManager().getHabboInfo(userId); if (habboInfo != null) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionAlertEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionAlertEvent.java index c37f1f52..835ae6f7 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionAlertEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionAlertEvent.java @@ -24,7 +24,7 @@ public class ModToolSanctionAlertEvent extends MessageHandler { String message = ModToolInputGuard.normalize(this.packet.readString()); int cfhTopic = this.packet.readInt(); - if (!ModToolInputGuard.isSafeMessage(message)) { + if (!ModToolTicketGuard.isPositiveId(userId) || !ModToolTicketGuard.isPositiveId(cfhTopic) || !ModToolInputGuard.isSafeMessage(message)) { return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionBanEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionBanEvent.java index d23f943a..f6bb775a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionBanEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionBanEvent.java @@ -37,7 +37,7 @@ public class ModToolSanctionBanEvent extends MessageHandler { int duration = 0; - if (!ModToolInputGuard.isSafeMessage(message)) { + if (!ModToolTicketGuard.isPositiveId(userId) || !ModToolTicketGuard.isPositiveId(cfhTopic) || !ModToolInputGuard.isSafeMessage(message)) { return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionMuteEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionMuteEvent.java index e5f91ab2..ea2604a4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionMuteEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionMuteEvent.java @@ -26,7 +26,7 @@ public class ModToolSanctionMuteEvent extends MessageHandler { String message = ModToolInputGuard.normalize(this.packet.readString()); int cfhTopic = this.packet.readInt(); - if (!ModToolInputGuard.isSafeMessage(message)) { + if (!ModToolTicketGuard.isPositiveId(userId) || !ModToolTicketGuard.isPositiveId(cfhTopic) || !ModToolInputGuard.isSafeMessage(message)) { return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionTradeLockEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionTradeLockEvent.java index b49134f6..546a05a3 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionTradeLockEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionTradeLockEvent.java @@ -25,7 +25,7 @@ public class ModToolSanctionTradeLockEvent extends MessageHandler { int duration = this.packet.readInt(); int cfhTopic = this.packet.readInt(); - if (!ModToolInputGuard.isSafeMessage(message)) { + if (!ModToolTicketGuard.isPositiveId(userId) || !ModToolTicketGuard.isPositiveId(cfhTopic) || !ModToolInputGuard.isSafeMessage(message)) { return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolWarnEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolWarnEvent.java index 06c118b2..eead312a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolWarnEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolWarnEvent.java @@ -19,7 +19,7 @@ public class ModToolWarnEvent extends MessageHandler { int userId = this.packet.readInt(); String message = ModToolInputGuard.normalize(this.packet.readString()); - if (!ModToolInputGuard.isSafeMessage(message)) { + if (!ModToolTicketGuard.isPositiveId(userId) || !ModToolInputGuard.isSafeMessage(message)) { return; } diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolPermissionContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolPermissionContractTest.java index ba03fc67..ce1d6a4b 100644 --- a/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolPermissionContractTest.java +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolPermissionContractTest.java @@ -109,4 +109,28 @@ class ModToolPermissionContractTest { handler + " must reject empty or oversized staff-supplied text"); } } + + @Test + void staffSuppliedModToolTargetsArePositiveBeforeLookupOrMutation() throws Exception { + Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool"); + + for (String handler : List.of( + "ModToolAlertEvent.java", + "ModToolWarnEvent.java", + "ModToolKickEvent.java", + "ModToolChangeRoomSettingsEvent.java", + "ModToolRequestRoomInfoEvent.java", + "ModToolRequestRoomVisitsEvent.java", + "ModToolIssueDefaultSanctionEvent.java", + "ModToolSanctionAlertEvent.java", + "ModToolSanctionBanEvent.java", + "ModToolSanctionMuteEvent.java", + "ModToolSanctionTradeLockEvent.java" + )) { + String source = Files.readString(base.resolve(handler)); + + assertTrue(source.contains("ModToolTicketGuard.isPositiveId"), + handler + " must reject zero or negative client-provided ids before manager/database lookups"); + } + } } From 032003b64c4e5336300c8f3120836c4a8250e361 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 21:25:55 +0200 Subject: [PATCH 16/18] fix(commands): enforce staff target ceilings --- .../habbohotel/commands/AlertCommand.java | 5 ++ .../habbo/habbohotel/commands/BanCommand.java | 2 +- .../commands/CommandTargetGuard.java | 46 +++++++++++++ .../commands/DisconnectCommand.java | 2 +- .../commands/GivePrefixCommand.java | 5 ++ .../habbohotel/commands/GiveRankCommand.java | 6 +- .../habbohotel/commands/IPBanCommand.java | 2 +- .../commands/MachineBanCommand.java | 2 +- .../habbohotel/commands/MuteCommand.java | 5 ++ .../commands/RemovePrefixCommand.java | 5 ++ .../habbohotel/commands/SuperbanCommand.java | 4 +- .../habbohotel/commands/UnmuteCommand.java | 5 ++ .../CommandTargetGuardContractTest.java | 64 +++++++++++++++++++ 13 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandTargetGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/commands/CommandTargetGuardContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/AlertCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/AlertCommand.java index 17c2042f..eec564c1 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/AlertCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/AlertCommand.java @@ -32,6 +32,11 @@ public class AlertCommand extends Command { Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(targetUsername); if (habbo != null) { + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); + return true; + } + habbo.alert(message + "\r\n -" + gameClient.getHabbo().getHabboInfo().getUsername()); gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_alert.message_send").replace("%user%", targetUsername), RoomChatMessageBubbles.ALERT); } else { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/BanCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/BanCommand.java index 90b2ea57..61e66d50 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/BanCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/BanCommand.java @@ -60,7 +60,7 @@ public class BanCommand extends Command { return true; } - if (target.getRank().getId() >= gameClient.getHabbo().getHabboInfo().getRank().getId()) { + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), target)) { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); return true; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandTargetGuard.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandTargetGuard.java new file mode 100644 index 00000000..cd7b97b3 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandTargetGuard.java @@ -0,0 +1,46 @@ +package com.eu.habbo.habbohotel.commands; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Rank; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboInfo; + +final class CommandTargetGuard { + private CommandTargetGuard() { + } + + static boolean canTarget(Habbo moderator, Habbo target) { + return target != null && canTarget(moderator, target.getHabboInfo()); + } + + static boolean canTarget(Habbo moderator, HabboInfo target) { + if (moderator == null || target == null || moderator.getHabboInfo().getId() == target.getId()) { + return false; + } + + int moderatorRankId = moderator.getHabboInfo().getRank().getId(); + int targetRankId = target.getRank().getId(); + + return targetRankId < moderatorRankId || isCoreRank(moderatorRankId) && targetRankId <= moderatorRankId; + } + + static boolean canAssignRank(Habbo moderator, Rank rank) { + if (moderator == null || rank == null) { + return false; + } + + int moderatorRankId = moderator.getHabboInfo().getRank().getId(); + int targetRankId = rank.getId(); + + return targetRankId < moderatorRankId || isCoreRank(moderatorRankId) && targetRankId <= moderatorRankId; + } + + private static boolean isCoreRank(int rankId) { + int highestRankId = Emulator.getGameEnvironment().getPermissionsManager().getAllRanks().stream() + .mapToInt(Rank::getId) + .max() + .orElse(0); + + return highestRankId > 0 && rankId >= highestRankId; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/DisconnectCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/DisconnectCommand.java index 82dd200e..b7f0eed8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/DisconnectCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/DisconnectCommand.java @@ -29,7 +29,7 @@ public class DisconnectCommand extends Command { return true; } - if (target.getHabboInfo().getRank().getId() > gameClient.getHabbo().getHabboInfo().getRank().getId()) { + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), target)) { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_disconnect.higher_rank"), RoomChatMessageBubbles.ALERT); return true; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GivePrefixCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GivePrefixCommand.java index eba65376..30201488 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GivePrefixCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GivePrefixCommand.java @@ -47,6 +47,11 @@ public class GivePrefixCommand extends Command { return true; } + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), target)) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); + return true; + } + UserPrefix prefix = new UserPrefix(target.getHabboInfo().getId(), text, color, icon, effect); prefix.run(); target.getInventory().getPrefixesComponent().addPrefix(prefix); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GiveRankCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GiveRankCommand.java index bfe0cfb1..12a8c247 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GiveRankCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GiveRankCommand.java @@ -36,7 +36,7 @@ public class GiveRankCommand extends Command { } if (rank != null) { - if (rank.getId() > gameClient.getHabbo().getHabboInfo().getRank().getId()) { + if (!CommandTargetGuard.canAssignRank(gameClient.getHabbo(), rank)) { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_rank.higher").replace("%username%", params[1]).replace("%id%", rank.getName()), RoomChatMessageBubbles.ALERT); return true; } @@ -44,7 +44,7 @@ public class GiveRankCommand extends Command { HabboInfo habbo = HabboManager.getOfflineHabboInfo(params[1]); if (habbo != null) { - if (habbo.getRank().getId() > gameClient.getHabbo().getHabboInfo().getRank().getId()) { + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_rank.higher.other").replace("%username%", params[1]).replace("%id%", rank.getName()), RoomChatMessageBubbles.ALERT); return true; } @@ -63,4 +63,4 @@ public class GiveRankCommand extends Command { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.errors.cmd_give_rank.not_found").replace("%id%", params[2]).replace("%username%", params[1]), RoomChatMessageBubbles.ALERT); return true; } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/IPBanCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/IPBanCommand.java index 8622cec9..8c9af239 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/IPBanCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/IPBanCommand.java @@ -47,7 +47,7 @@ public class IPBanCommand extends Command { return true; } - if (habbo.getRank().getId() >= gameClient.getHabbo().getHabboInfo().getRank().getId()) { + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); return true; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MachineBanCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MachineBanCommand.java index 29918bb9..5fa15c9f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MachineBanCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MachineBanCommand.java @@ -43,7 +43,7 @@ public class MachineBanCommand extends Command { return true; } - if (habbo.getRank().getId() >= gameClient.getHabbo().getHabboInfo().getRank().getId()) { + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); return true; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MuteCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MuteCommand.java index 222c8cf3..8b1682fb 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MuteCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MuteCommand.java @@ -29,6 +29,11 @@ public class MuteCommand extends Command { return true; } + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); + return true; + } + int duration = Integer.MAX_VALUE; if (params.length == 3) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RemovePrefixCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RemovePrefixCommand.java index 9396d18d..ddf8d024 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RemovePrefixCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RemovePrefixCommand.java @@ -31,6 +31,11 @@ public class RemovePrefixCommand extends Command { return true; } + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), target)) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); + return true; + } + if (prefixIdStr.equalsIgnoreCase("all")) { List prefixes = target.getInventory().getPrefixesComponent().getPrefixes(); for (UserPrefix prefix : prefixes) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/SuperbanCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/SuperbanCommand.java index 8f9d8968..1a4d44e3 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/SuperbanCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/SuperbanCommand.java @@ -41,7 +41,7 @@ public class SuperbanCommand extends Command { return true; } - if (habbo.getRank().getId() >= gameClient.getHabbo().getHabboInfo().getRank().getId()) { + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); return true; } @@ -56,4 +56,4 @@ public class SuperbanCommand extends Command { return true; } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/UnmuteCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/UnmuteCommand.java index 7cd922e9..ba563dd5 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/UnmuteCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/UnmuteCommand.java @@ -23,6 +23,11 @@ public class UnmuteCommand extends Command { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_unmute.not_found").replace("%user%", params[1]), RoomChatMessageBubbles.ALERT); return true; } else { + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); + return true; + } + if (!habbo.getHabboStats().allowTalk() || (habbo.getHabboInfo().getCurrentRoom() != null && habbo.getHabboInfo().getCurrentRoom().isMuted(habbo))) { if (!habbo.getHabboStats().allowTalk()) { habbo.unMute(); diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/commands/CommandTargetGuardContractTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/commands/CommandTargetGuardContractTest.java new file mode 100644 index 00000000..57aa369e --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/commands/CommandTargetGuardContractTest.java @@ -0,0 +1,64 @@ +package com.eu.habbo.habbohotel.commands; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CommandTargetGuardContractTest { + @Test + void highRiskUserCommandsUseCentralTargetGuard() throws Exception { + Path base = Path.of("src/main/java/com/eu/habbo/habbohotel/commands"); + + for (String command : List.of( + "AlertCommand.java", + "BanCommand.java", + "DisconnectCommand.java", + "GivePrefixCommand.java", + "GiveRankCommand.java", + "IPBanCommand.java", + "MachineBanCommand.java", + "MuteCommand.java", + "RemovePrefixCommand.java", + "SuperbanCommand.java", + "UnmuteCommand.java" + )) { + String source = Files.readString(base.resolve(command)); + + assertTrue(source.contains("CommandTargetGuard.canTarget"), + command + " must use the central command target guard for staff/core rank handling"); + } + } + + @Test + void rankGrantingUsesCentralAssignmentGuard() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/commands/GiveRankCommand.java")); + + assertTrue(source.contains("CommandTargetGuard.canAssignRank"), + "GiveRankCommand must guard the assigned rank with the same core-rank semantics"); + } + + @Test + void targetGuardKeepsCorePeerOverrideCentralized() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/commands/CommandTargetGuard.java")); + String rule = "targetRankId < moderatorRankId || isCoreRank(moderatorRankId) && targetRankId <= moderatorRankId"; + + assertTrue(countOccurrences(source, rule) >= 2, + "non-core command users must only target lower ranks while the highest/core rank may target peer ranks"); + } + + private static int countOccurrences(String source, String needle) { + int count = 0; + int index = 0; + + while ((index = source.indexOf(needle, index)) >= 0) { + count++; + index += needle.length(); + } + + return count; + } +} From efe7897fb4da3dbb316b39c71dc4efc23e40c688 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 21:30:37 +0200 Subject: [PATCH 17/18] fix(catalog): bound marketplace inputs --- .../catalog/marketplace/MarketPlace.java | 4 ++ .../catalog/marketplace/BuyItemEvent.java | 4 ++ .../marketplace/MarketplaceInputGuard.java | 47 +++++++++++++++++ .../marketplace/RequestItemInfoEvent.java | 4 ++ .../marketplace/RequestOffersEvent.java | 12 ++--- .../catalog/marketplace/SellItemEvent.java | 1 + .../marketplace/TakeBackItemEvent.java | 5 ++ .../MarketplaceInputContractTest.java | 50 +++++++++++++++++++ .../MarketplaceInputGuardTest.java | 42 ++++++++++++++++ 9 files changed, 161 insertions(+), 8 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/MarketplaceInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/marketplace/MarketplaceInputContractTest.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/marketplace/MarketplaceInputGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java index b9d0b7ab..5c1592b9 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java @@ -58,6 +58,10 @@ public class MarketPlace { public static void takeBackItem(Habbo habbo, int offerId) { MarketPlaceOffer offer = habbo.getInventory().getOffer(offerId); + if (offer == null) { + return; + } + if (!Emulator.getPluginManager().fireEvent(new MarketPlaceItemCancelledEvent(offer)).isCancelled()) { takeBackItem(habbo, offer); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/BuyItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/BuyItemEvent.java index 5d604fb4..1261c4d7 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/BuyItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/BuyItemEvent.java @@ -13,6 +13,10 @@ public class BuyItemEvent extends MessageHandler { public void handle() throws Exception { int offerId = this.packet.readInt(); + if (!MarketplaceInputGuard.isPositiveId(offerId)) { + return; + } + MarketPlace.buyItem(offerId, this.client); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/MarketplaceInputGuard.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/MarketplaceInputGuard.java new file mode 100644 index 00000000..85440bbf --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/MarketplaceInputGuard.java @@ -0,0 +1,47 @@ +package com.eu.habbo.messages.incoming.catalog.marketplace; + +import com.eu.habbo.habbohotel.catalog.marketplace.MarketPlace; + +final class MarketplaceInputGuard { + static final int MAX_SEARCH_LENGTH = 30; + static final int DEFAULT_SORT = 1; + static final int MIN_SORT = 1; + static final int MAX_SORT = 6; + + private MarketplaceInputGuard() { + } + + static boolean isPositiveId(int id) { + return id > 0; + } + + static String normalizeSearch(String query) { + if (query == null) { + return ""; + } + + String normalized = query.trim(); + return normalized.length() > MAX_SEARCH_LENGTH ? normalized.substring(0, MAX_SEARCH_LENGTH) : normalized; + } + + static int normalizeSort(int sort) { + return sort >= MIN_SORT && sort <= MAX_SORT ? sort : DEFAULT_SORT; + } + + static int normalizeMinPrice(int minPrice) { + if (minPrice == -1) { + return -1; + } + + return Math.max(0, Math.min(minPrice, MarketPlace.MAXIMUM_LISTING_PRICE)); + } + + static int normalizeMaxPrice(int maxPrice, int minPrice) { + if (maxPrice == -1) { + return -1; + } + + int normalized = Math.max(0, Math.min(maxPrice, MarketPlace.MAXIMUM_LISTING_PRICE)); + return minPrice > 0 && normalized > 0 && normalized < minPrice ? minPrice : normalized; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/RequestItemInfoEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/RequestItemInfoEvent.java index 45c043f6..65011215 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/RequestItemInfoEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/RequestItemInfoEvent.java @@ -9,6 +9,10 @@ public class RequestItemInfoEvent extends MessageHandler { this.packet.readInt(); int id = this.packet.readInt(); + if (!MarketplaceInputGuard.isPositiveId(id)) { + return; + } + this.client.sendResponse(new MarketplaceItemInfoComposer(id)); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/RequestOffersEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/RequestOffersEvent.java index ccd05b35..260e4166 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/RequestOffersEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/RequestOffersEvent.java @@ -20,14 +20,10 @@ public class RequestOffersEvent extends MessageHandler { @Override public void handle() throws Exception { - int min = this.packet.readInt(); - int max = this.packet.readInt(); - String query = this.packet.readString(); - int type = this.packet.readInt(); - - if (query.length() > 30) { - query = query.substring(0, 30); - } + int min = MarketplaceInputGuard.normalizeMinPrice(this.packet.readInt()); + int max = MarketplaceInputGuard.normalizeMaxPrice(this.packet.readInt(), min); + String query = MarketplaceInputGuard.normalizeSearch(this.packet.readString()); + int type = MarketplaceInputGuard.normalizeSort(this.packet.readInt()); boolean tryCache = min == -1 && max == -1 && query.isEmpty(); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/SellItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/SellItemEvent.java index 65ce7d6f..5eecceb2 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/SellItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/SellItemEvent.java @@ -29,6 +29,7 @@ public class SellItemEvent extends MessageHandler { final int furniType = this.packet.readInt(); // 1 = FLOOR_TYPE, 2 = WALL_TYPE final int itemId = this.packet.readInt(); + if (!MarketplaceInputGuard.isPositiveId(itemId)) return; if (furniType != 1 && furniType != 2) return; HabboItem item = this.client.getHabbo().getInventory().getItemsComponent().getHabboItem(itemId); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/TakeBackItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/TakeBackItemEvent.java index 12e7b1ef..2fc46067 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/TakeBackItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/TakeBackItemEvent.java @@ -12,6 +12,11 @@ public class TakeBackItemEvent extends MessageHandler { @Override public void handle() throws Exception { int offerId = this.packet.readInt(); + + if (!MarketplaceInputGuard.isPositiveId(offerId)) { + return; + } + MarketPlace.takeBackItem(this.client.getHabbo(), offerId); } } diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/marketplace/MarketplaceInputContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/marketplace/MarketplaceInputContractTest.java new file mode 100644 index 00000000..54262497 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/marketplace/MarketplaceInputContractTest.java @@ -0,0 +1,50 @@ +package com.eu.habbo.messages.incoming.catalog.marketplace; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MarketplaceInputContractTest { + @Test + void marketplaceIdHandlersRejectNonPositiveIds() throws Exception { + Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace"); + + for (String handler : List.of( + "BuyItemEvent.java", + "RequestItemInfoEvent.java", + "SellItemEvent.java", + "TakeBackItemEvent.java" + )) { + String source = Files.readString(base.resolve(handler)); + + assertTrue(source.contains("MarketplaceInputGuard.isPositiveId"), + handler + " must reject zero or negative ids before marketplace or inventory lookup"); + } + } + + @Test + void offerSearchNormalizesCacheKeyInputs() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/catalog/marketplace/RequestOffersEvent.java")); + + assertTrue(source.contains("MarketplaceInputGuard.normalizeMinPrice"), + "marketplace offer search must normalize minimum price"); + assertTrue(source.contains("MarketplaceInputGuard.normalizeMaxPrice"), + "marketplace offer search must normalize maximum price"); + assertTrue(source.contains("MarketplaceInputGuard.normalizeSearch"), + "marketplace offer search must trim and bound search text"); + assertTrue(source.contains("MarketplaceInputGuard.normalizeSort"), + "marketplace offer search must normalize sort before using it as a cache key"); + } + + @Test + void takeBackDoesNotFirePluginEventForMissingOffer() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java")); + + assertTrue(source.contains("if (offer == null)"), + "takeBackItem must ignore missing offers before constructing plugin events"); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/marketplace/MarketplaceInputGuardTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/marketplace/MarketplaceInputGuardTest.java new file mode 100644 index 00000000..f772fec9 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/catalog/marketplace/MarketplaceInputGuardTest.java @@ -0,0 +1,42 @@ +package com.eu.habbo.messages.incoming.catalog.marketplace; + +import com.eu.habbo.habbohotel.catalog.marketplace.MarketPlace; +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 MarketplaceInputGuardTest { + @Test + void idsMustBePositive() { + assertFalse(MarketplaceInputGuard.isPositiveId(0)); + assertFalse(MarketplaceInputGuard.isPositiveId(-1)); + assertTrue(MarketplaceInputGuard.isPositiveId(1)); + } + + @Test + void searchIsTrimmedAndBounded() { + assertEquals("", MarketplaceInputGuard.normalizeSearch(null)); + assertEquals("rare", MarketplaceInputGuard.normalizeSearch(" rare ")); + assertEquals(MarketplaceInputGuard.MAX_SEARCH_LENGTH, MarketplaceInputGuard.normalizeSearch("a".repeat(80)).length()); + } + + @Test + void sortFallsBackToDefaultOutsideKnownRange() { + assertEquals(MarketplaceInputGuard.DEFAULT_SORT, MarketplaceInputGuard.normalizeSort(0)); + assertEquals(3, MarketplaceInputGuard.normalizeSort(3)); + assertEquals(MarketplaceInputGuard.DEFAULT_SORT, MarketplaceInputGuard.normalizeSort(7)); + } + + @Test + void priceRangesPreserveCacheSentinelAndStayBounded() { + assertEquals(-1, MarketplaceInputGuard.normalizeMinPrice(-1)); + assertEquals(0, MarketplaceInputGuard.normalizeMinPrice(-100)); + assertEquals(MarketPlace.MAXIMUM_LISTING_PRICE, MarketplaceInputGuard.normalizeMinPrice(Integer.MAX_VALUE)); + + assertEquals(-1, MarketplaceInputGuard.normalizeMaxPrice(-1, -1)); + assertEquals(500, MarketplaceInputGuard.normalizeMaxPrice(100, 500)); + assertEquals(MarketPlace.MAXIMUM_LISTING_PRICE, MarketplaceInputGuard.normalizeMaxPrice(Integer.MAX_VALUE, 0)); + } +} From 62454671d2e031f7c00daa5ace753ca5786b44d4 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 21:36:06 +0200 Subject: [PATCH 18/18] fix(navigator): bound search inputs --- .../navigator/AddSavedSearchEvent.java | 7 +-- .../navigator/NavigatorInputGuard.java | 26 +++++++++++ .../navigator/SearchRoomsByTagEvent.java | 2 +- .../incoming/navigator/SearchRoomsEvent.java | 45 ++++++++++++------- .../navigator/NavigatorInputGuardTest.java | 21 +++++++++ .../NavigatorSearchInputContractTest.java | 37 +++++++++++++++ 6 files changed, 115 insertions(+), 23 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/NavigatorInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/navigator/NavigatorInputGuardTest.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/navigator/NavigatorSearchInputContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/AddSavedSearchEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/AddSavedSearchEvent.java index 53cf4e3d..b397f3fd 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/AddSavedSearchEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/AddSavedSearchEvent.java @@ -14,11 +14,8 @@ public class AddSavedSearchEvent extends MessageHandler { @Override public void handle() throws Exception { - String searchCode = this.packet.readString(); - String filter = this.packet.readString(); - - if (searchCode.length() > 255) searchCode = searchCode.substring(0, 255); - if (filter.length() > 255) filter = filter.substring(0, 255); + String searchCode = NavigatorInputGuard.normalizeSavedSearchValue(this.packet.readString()); + String filter = NavigatorInputGuard.normalizeSavedSearchValue(this.packet.readString()); if (this.client.getHabbo().getHabboInfo().getSavedSearches().size() >= MAX_SAVED_SEARCHES) { this.client.sendResponse(new NewNavigatorSavedSearchesComposer(this.client.getHabbo().getHabboInfo().getSavedSearches())); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/NavigatorInputGuard.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/NavigatorInputGuard.java new file mode 100644 index 00000000..434f4733 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/NavigatorInputGuard.java @@ -0,0 +1,26 @@ +package com.eu.habbo.messages.incoming.navigator; + +final class NavigatorInputGuard { + static final int MAX_SEARCH_LENGTH = 64; + static final int MAX_SAVED_SEARCH_LENGTH = 255; + + private NavigatorInputGuard() { + } + + static String normalizeSearch(String value) { + return normalize(value, MAX_SEARCH_LENGTH); + } + + static String normalizeSavedSearchValue(String value) { + return normalize(value, MAX_SAVED_SEARCH_LENGTH); + } + + private static String normalize(String value, int maxLength) { + if (value == null) { + return ""; + } + + String normalized = value.trim(); + return normalized.length() > maxLength ? normalized.substring(0, maxLength) : normalized; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/SearchRoomsByTagEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/SearchRoomsByTagEvent.java index 0d572edf..cf18a821 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/SearchRoomsByTagEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/SearchRoomsByTagEvent.java @@ -7,7 +7,7 @@ import com.eu.habbo.messages.outgoing.navigator.PrivateRoomsComposer; public class SearchRoomsByTagEvent extends MessageHandler { @Override public void handle() throws Exception { - String tag = this.packet.readString(); + String tag = NavigatorInputGuard.normalizeSearch(this.packet.readString()); this.client.sendResponse(new PrivateRoomsComposer(Emulator.getGameEnvironment().getRoomManager().getRoomsWithTag(tag))); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/SearchRoomsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/SearchRoomsEvent.java index 98d824e7..3fc13ffc 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/SearchRoomsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/navigator/SearchRoomsEvent.java @@ -34,36 +34,43 @@ public class SearchRoomsEvent extends MessageHandler { @Override public void handle() throws Exception { - String name = this.packet.readString(); + String name = NavigatorInputGuard.normalizeSearch(this.packet.readString()); String prefix = ""; String query = name; ArrayList rooms; + if (name.startsWith("owner:")) { + query = NavigatorInputGuard.normalizeSearch(name.substring("owner:".length())); + prefix = "owner:"; + } else if (name.startsWith("tag:")) { + query = NavigatorInputGuard.normalizeSearch(name.substring("tag:".length())); + prefix = "tag:"; + } else if (name.startsWith("group:")) { + query = NavigatorInputGuard.normalizeSearch(name.substring("group:".length())); + prefix = "group:"; + } + + String cacheKey = buildCacheKey(prefix, query); + ServerMessage message = null; Map rankCache = cachedResults.get(this.client.getHabbo().getHabboInfo().getRank()); if (rankCache != null) { - message = rankCache.get((name + "\t" + query).toLowerCase()); + message = rankCache.get(cacheKey); } else { rankCache = createLRUCache(); cachedResults.put(this.client.getHabbo().getHabboInfo().getRank(), rankCache); } if (message == null) { - if (name.startsWith("owner:")) { - query = name.split("owner:")[1]; - prefix = "owner:"; - rooms = (ArrayList) Emulator.getGameEnvironment().getRoomManager().getRoomsForHabbo(name); - } else if (name.startsWith("tag:")) { - query = name.split("tag:")[1]; - prefix = "tag:"; - rooms = Emulator.getGameEnvironment().getRoomManager().getRoomsWithTag(name); - } else if (name.startsWith("group:")) { - query = name.split("group:")[1]; - prefix = "group:"; - rooms = Emulator.getGameEnvironment().getRoomManager().getGroupRoomsWithName(name); + if (prefix.equals("owner:")) { + rooms = (ArrayList) Emulator.getGameEnvironment().getRoomManager().getRoomsForHabbo(query); + } else if (prefix.equals("tag:")) { + rooms = Emulator.getGameEnvironment().getRoomManager().getRoomsWithTag(query); + } else if (prefix.equals("group:")) { + rooms = Emulator.getGameEnvironment().getRoomManager().getGroupRoomsWithName(query); } else { - rooms = Emulator.getGameEnvironment().getRoomManager().getRoomsWithName(name); + rooms = Emulator.getGameEnvironment().getRoomManager().getRoomsWithName(query); } message = new PrivateRoomsComposer(rooms).compose(); @@ -73,7 +80,7 @@ public class SearchRoomsEvent extends MessageHandler { map = createLRUCache(); } - map.put((name + "\t" + query).toLowerCase(), message); + map.put(cacheKey, message); cachedResults.put(this.client.getHabbo().getHabboInfo().getRank(), map); NavigatorSearchResultEvent event = new NavigatorSearchResultEvent(this.client.getHabbo(), prefix, query, rooms); @@ -84,4 +91,8 @@ public class SearchRoomsEvent extends MessageHandler { this.client.sendResponse(message); } -} \ No newline at end of file + + private static String buildCacheKey(String prefix, String query) { + return (prefix + "\t" + query).toLowerCase(); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/navigator/NavigatorInputGuardTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/navigator/NavigatorInputGuardTest.java new file mode 100644 index 00000000..4bb3b32b --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/navigator/NavigatorInputGuardTest.java @@ -0,0 +1,21 @@ +package com.eu.habbo.messages.incoming.navigator; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class NavigatorInputGuardTest { + @Test + void searchValuesAreTrimmedAndBounded() { + assertEquals("", NavigatorInputGuard.normalizeSearch(null)); + assertEquals("rare", NavigatorInputGuard.normalizeSearch(" rare ")); + assertEquals(NavigatorInputGuard.MAX_SEARCH_LENGTH, NavigatorInputGuard.normalizeSearch("a".repeat(100)).length()); + } + + @Test + void savedSearchValuesUseLargerBound() { + assertEquals("", NavigatorInputGuard.normalizeSavedSearchValue(null)); + assertEquals("owner:duckie", NavigatorInputGuard.normalizeSavedSearchValue(" owner:duckie ")); + assertEquals(NavigatorInputGuard.MAX_SAVED_SEARCH_LENGTH, NavigatorInputGuard.normalizeSavedSearchValue("a".repeat(400)).length()); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/navigator/NavigatorSearchInputContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/navigator/NavigatorSearchInputContractTest.java new file mode 100644 index 00000000..db44b6a7 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/navigator/NavigatorSearchInputContractTest.java @@ -0,0 +1,37 @@ +package com.eu.habbo.messages.incoming.navigator; + +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 NavigatorSearchInputContractTest { + @Test + void classicSearchNormalizesInputAndPassesUnprefixedQueriesToManagers() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/navigator/SearchRoomsEvent.java")); + + assertTrue(source.contains("NavigatorInputGuard.normalizeSearch(this.packet.readString())"), + "classic room search must normalize raw client text before cache or manager lookups"); + assertTrue(source.contains("getRoomsForHabbo(query)"), + "owner search must pass only the unprefixed owner query"); + assertTrue(source.contains("getRoomsWithTag(query)"), + "tag search must pass only the unprefixed tag query"); + assertTrue(source.contains("getGroupRoomsWithName(query)"), + "group search must pass only the unprefixed group query"); + assertTrue(source.contains("buildCacheKey(prefix, query)"), + "classic room search must cache using normalized prefix/query pairs"); + } + + @Test + void savedAndTagSearchesNormalizeText() throws Exception { + String saved = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/navigator/AddSavedSearchEvent.java")); + String tag = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/navigator/SearchRoomsByTagEvent.java")); + + assertTrue(saved.contains("NavigatorInputGuard.normalizeSavedSearchValue"), + "saved searches must trim and bound search code/filter values"); + assertTrue(tag.contains("NavigatorInputGuard.normalizeSearch"), + "tag search must trim and bound tag values"); + } +}