From 37ce71ad1ed13cbdd335f85b4b514c731646f642 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 15 Jun 2026 22:19:29 +0200 Subject: [PATCH 01/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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"); + } +} From a37de4556ba2663a0295f58c013bceba3e5559c7 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 21:44:10 +0200 Subject: [PATCH 19/43] fix(gameclients): bound login session inputs --- .../habbohotel/gameclients/GameClient.java | 6 +++++ .../incoming/handshake/SecureLoginEvent.java | 14 +++++----- .../handshake/SecureLoginInputGuard.java | 20 ++++++++++++++ .../GameClientManagerContractTest.java | 8 ++++++ .../handshake/SecureLoginInputGuardTest.java | 27 +++++++++++++++++++ 5 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/handshake/SecureLoginInputGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java index 87d5fb3d..a3af0226 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/gameclients/GameClient.java @@ -14,6 +14,7 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; public class GameClient { @@ -24,6 +25,7 @@ public class GameClient { private final LatencyTracker latencyTracker; private Habbo habbo; + private final AtomicBoolean disposed = new AtomicBoolean(false); private boolean handshakeFinished; private String machineId = ""; private String ssoTicket = ""; @@ -153,6 +155,10 @@ public class GameClient { } public void dispose(boolean allowSessionResume) { + if (!this.disposed.compareAndSet(false, true)) { + return; + } + try { this.channel.close(); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java index f9704bec..c65abab3 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginEvent.java @@ -72,7 +72,13 @@ public class SecureLoginEvent extends MessageHandler { return; } - String sso = this.packet.readString().replace(" ", ""); + String sso = SecureLoginInputGuard.normalizeSsoTicket(this.packet.readString()); + + if (!SecureLoginInputGuard.isValidSsoTicket(sso)) { + Emulator.getGameServer().getGameClientManager().disposeClient(this.client); + LOGGER.debug("Client is trying to connect with an invalid SSO ticket! Closed connection..."); + return; + } if (Emulator.getPluginManager().fireEvent(new SSOAuthenticationEvent(sso)).isCancelled()) { Emulator.getGameServer().getGameClientManager().disposeClient(this.client); @@ -80,12 +86,6 @@ public class SecureLoginEvent extends MessageHandler { return; } - if (sso.isEmpty()) { - Emulator.getGameServer().getGameClientManager().disposeClient(this.client); - LOGGER.debug("Client is trying to connect without SSO ticket! Closed connection..."); - return; - } - if (this.client.getHabbo() == null) { // Store SSO ticket on client for grace period tracking this.client.setSsoTicket(sso); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginInputGuard.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginInputGuard.java new file mode 100644 index 00000000..b9a5a9b0 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/handshake/SecureLoginInputGuard.java @@ -0,0 +1,20 @@ +package com.eu.habbo.messages.incoming.handshake; + +final class SecureLoginInputGuard { + static final int MAX_SSO_TICKET_LENGTH = 512; + + private SecureLoginInputGuard() { + } + + static String normalizeSsoTicket(String ticket) { + if (ticket == null) { + return ""; + } + + return ticket.replace(" ", ""); + } + + static boolean isValidSsoTicket(String ticket) { + return ticket != null && !ticket.isEmpty() && ticket.length() <= MAX_SSO_TICKET_LENGTH; + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/gameclients/GameClientManagerContractTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/gameclients/GameClientManagerContractTest.java index 4f35e668..27bc393f 100644 --- a/Emulator/src/test/java/com/eu/habbo/habbohotel/gameclients/GameClientManagerContractTest.java +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/gameclients/GameClientManagerContractTest.java @@ -3,6 +3,7 @@ package com.eu.habbo.habbohotel.gameclients; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; class GameClientManagerContractTest { @@ -19,4 +20,11 @@ class GameClientManagerContractTest { assertDoesNotThrow(() -> manager.disposeClient(null)); assertDoesNotThrow(() -> manager.forceDisposeClient(null)); } + + @Test + void gameClientDisposeIsExplicitlyIdempotent() throws Exception { + assertTrue(java.util.concurrent.atomic.AtomicBoolean.class.isAssignableFrom( + GameClient.class.getDeclaredField("disposed").getType() + )); + } } diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/handshake/SecureLoginInputGuardTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/handshake/SecureLoginInputGuardTest.java new file mode 100644 index 00000000..28ec4504 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/handshake/SecureLoginInputGuardTest.java @@ -0,0 +1,27 @@ +package com.eu.habbo.messages.incoming.handshake; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SecureLoginInputGuardTest { + + @Test + void normalizesNullAndSpacesBeforeAuthentication() { + assertEquals("", SecureLoginInputGuard.normalizeSsoTicket(null)); + assertEquals("abc123", SecureLoginInputGuard.normalizeSsoTicket(" abc 123 ")); + } + + @Test + void rejectsMissingOrOversizedTickets() { + assertFalse(SecureLoginInputGuard.isValidSsoTicket("")); + assertFalse(SecureLoginInputGuard.isValidSsoTicket("x".repeat(SecureLoginInputGuard.MAX_SSO_TICKET_LENGTH + 1))); + } + + @Test + void acceptsTicketWithinBound() { + assertTrue(SecureLoginInputGuard.isValidSsoTicket("x".repeat(SecureLoginInputGuard.MAX_SSO_TICKET_LENGTH))); + } +} From 743cad8361fb9eea42d17406ba70c932e3747a74 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 21:48:45 +0200 Subject: [PATCH 20/43] fix(permissions): fail closed on stale ranks --- .../permissions/PermissionSetting.java | 4 ++ .../permissions/PermissionsManager.java | 43 +++++++++++++++---- .../eu/habbo/habbohotel/permissions/Rank.java | 4 ++ .../PermissionSettingContractTest.java | 21 +++++++++ .../RankPermissionContractTest.java | 35 +++++++++++++++ 5 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/permissions/PermissionSettingContractTest.java create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/permissions/RankPermissionContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionSetting.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionSetting.java index d064d3dc..ef25f39e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionSetting.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionSetting.java @@ -11,6 +11,10 @@ public enum PermissionSetting { ROOM_OWNER; public static PermissionSetting fromString(String value) { + if (value == null) { + return DISALLOWED; + } + switch (value) { case "1": return ALLOWED; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionsManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionsManager.java index 0286a468..6d354f1f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionsManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/PermissionsManager.java @@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory; import java.sql.*; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -65,35 +66,45 @@ public class PermissionsManager { } private void loadPermissionsLegacy(Connection connection) throws SQLException { + Set loadedRankIds = new HashSet<>(); + try (Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM permissions ORDER BY id ASC")) { while (set.next()) { + int rankId = set.getInt("id"); + loadedRankIds.add(rankId); + Rank rank = null; - if (!this.ranks.containsKey(set.getInt("id"))) { + if (!this.ranks.containsKey(rankId)) { rank = new Rank(set); - this.ranks.put(set.getInt("id"), rank); + this.ranks.put(rankId, rank); } else { - rank = this.ranks.get(set.getInt("id")); + rank = this.ranks.get(rankId); rank.load(set); } this.addBadgeMapping(rank); } } + + this.pruneMissingRanks(loadedRankIds); } private boolean loadPermissionsNormalized(Connection connection) throws SQLException { boolean hasRanks = false; List loadedRanks = new ArrayList<>(); + Set loadedRankIds = new HashSet<>(); try (Statement statement = connection.createStatement(); ResultSet set = statement.executeQuery("SELECT * FROM permission_ranks ORDER BY id ASC")) { while (set.next()) { hasRanks = true; + int rankId = set.getInt("id"); + loadedRankIds.add(rankId); - Rank rank = this.ranks.get(set.getInt("id")); + Rank rank = this.ranks.get(rankId); if (rank == null) { - rank = new Rank(set.getInt("id")); - this.ranks.put(set.getInt("id"), rank); + rank = new Rank(rankId); + this.ranks.put(rankId, rank); } rank.loadNormalizedMetadata(set); @@ -141,9 +152,18 @@ public class PermissionsManager { } } + this.pruneMissingRanks(loadedRankIds); return hasDefinitions; } + private void pruneMissingRanks(Set loadedRankIds) { + for (int rankId : this.ranks.keys()) { + if (!loadedRankIds.contains(rankId)) { + this.ranks.remove(rankId); + } + } + } + private void ensureNormalizedRankColumns(Connection connection, List loadedRanks) throws SQLException { Set availableColumns = new HashSet<>(); @@ -254,6 +274,10 @@ public class PermissionsManager { public boolean hasPermission(Habbo habbo, String permission, boolean withRoomRights) { + if (habbo == null || habbo.getHabboInfo() == null || permission == null || permission.isBlank()) { + return false; + } + if (!this.hasPermission(habbo.getHabboInfo().getRank(), permission, withRoomRights)) { for (HabboPlugin plugin : Emulator.getPluginManager().getPlugins()) { if (plugin.hasPermission(habbo, permission)) { @@ -269,15 +293,16 @@ public class PermissionsManager { public boolean hasPermission(Rank rank, String permission, boolean withRoomRights) { - return rank.hasPermission(permission, withRoomRights); + return rank != null && permission != null && !permission.isBlank() && rank.hasPermission(permission, withRoomRights); } public Set getStaffBadges() { - return this.badges.keySet(); + return Collections.unmodifiableSet(new HashSet<>(this.badges.keySet())); } public List getRanksByBadgeCode(String code) { - return this.badges.get(code); + List ranks = this.badges.get(code); + return ranks == null ? Collections.emptyList() : Collections.unmodifiableList(new ArrayList<>(ranks)); } public List getAllRanks() { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Rank.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Rank.java index 897e5b5b..188091ff 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Rank.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/permissions/Rank.java @@ -114,6 +114,10 @@ public class Rank { } public boolean hasPermission(String key, boolean isRoomOwner) { + if (key == null || key.isBlank()) { + return false; + } + if (this.permissions.containsKey(key)) { Permission permission = this.permissions.get(key); diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/permissions/PermissionSettingContractTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/permissions/PermissionSettingContractTest.java new file mode 100644 index 00000000..ba71f00f --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/permissions/PermissionSettingContractTest.java @@ -0,0 +1,21 @@ +package com.eu.habbo.habbohotel.permissions; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PermissionSettingContractTest { + + @Test + void unknownPermissionValuesFailClosed() { + assertEquals(PermissionSetting.DISALLOWED, PermissionSetting.fromString(null)); + assertEquals(PermissionSetting.DISALLOWED, PermissionSetting.fromString("")); + assertEquals(PermissionSetting.DISALLOWED, PermissionSetting.fromString("999")); + } + + @Test + void knownPermissionValuesMapToExplicitSettings() { + assertEquals(PermissionSetting.ALLOWED, PermissionSetting.fromString("1")); + assertEquals(PermissionSetting.ROOM_OWNER, PermissionSetting.fromString("2")); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/permissions/RankPermissionContractTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/permissions/RankPermissionContractTest.java new file mode 100644 index 00000000..06c34f19 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/permissions/RankPermissionContractTest.java @@ -0,0 +1,35 @@ +package com.eu.habbo.habbohotel.permissions; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RankPermissionContractTest { + + @Test + void missingPermissionsFailClosed() { + Rank rank = new Rank(1); + + assertFalse(rank.hasPermission(null, false)); + assertFalse(rank.hasPermission("", false)); + assertFalse(rank.hasPermission("acc_supporttool", false)); + } + + @Test + void roomOwnerPermissionOnlyPassesWithRoomRights() { + Rank rank = new Rank(1); + rank.setPermission("acc_placefurni", PermissionSetting.ROOM_OWNER); + + assertFalse(rank.hasPermission("acc_placefurni", false)); + assertTrue(rank.hasPermission("acc_placefurni", true)); + } + + @Test + void allowedPermissionPassesWithoutRoomRights() { + Rank rank = new Rank(1); + rank.setPermission("acc_supporttool", PermissionSetting.ALLOWED); + + assertTrue(rank.hasPermission("acc_supporttool", false)); + } +} From 1d7c5b856fc3849e5a1cfe082a6dc8a67139c1b7 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 21:53:39 +0200 Subject: [PATCH 21/43] fix(items): harden item data lookups --- .../com/eu/habbo/habbohotel/items/Item.java | 47 ++++++----- .../habbo/habbohotel/items/ItemDataGuard.java | 82 +++++++++++++++++++ .../habbo/habbohotel/items/ItemManager.java | 39 +++++++-- .../habbohotel/items/ItemDataGuardTest.java | 26 ++++++ 4 files changed, 163 insertions(+), 31 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemDataGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/ItemDataGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/Item.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/Item.java index 12478e75..063fbc1b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/Item.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/Item.java @@ -45,7 +45,7 @@ public class Item implements ISerialize { } public static boolean isPet(Item item) { - return item.getName().toLowerCase().startsWith("a0 pet"); + return item != null && item.getName() != null && item.getName().toLowerCase().startsWith("a0 pet"); } public static boolean isBot(Item item) { @@ -121,26 +121,19 @@ public class Item implements ISerialize { this.customParams = set.getString("customparams"); this.clothingOnWalk = set.getString("clothing_on_walk"); - if (!set.getString("vending_ids").isEmpty()) { + int[] vendingIds = ItemDataGuard.parsePositiveIntList(set.getString("vending_ids")); + if (vendingIds.length > 0) { this.vendingItems = new TIntArrayList(); - String[] vendingIds = set.getString("vending_ids").replace(";", ",").replace(".", ",").split(","); - for (String s : vendingIds) { - this.vendingItems.add(Integer.parseInt(s.replace(" ", ""))); + for (int vendingId : vendingIds) { + this.vendingItems.add(vendingId); } + } else { + this.vendingItems = new TIntArrayList(); } //if(this.interactionType.getType() == InteractionMultiHeight.class || this.interactionType.getType().isAssignableFrom(InteractionMultiHeight.class)) { - if (set.getString("multiheight").contains(";")) { - String[] s = set.getString("multiheight").split(";"); - this.multiHeights = new double[s.length]; - - for (int i = 0; i < s.length; i++) { - this.multiHeights[i] = Double.parseDouble(s[i]); - } - } else { - this.multiHeights = new double[0]; - } + this.multiHeights = ItemDataGuard.parseHeights(set.getString("multiheight")); } this.rotations = 4; @@ -254,6 +247,10 @@ public class Item implements ISerialize { } public int getRandomVendingItem() { + if (this.vendingItems == null || this.vendingItems.isEmpty()) { + return 0; + } + return this.vendingItems.get(Emulator.getRandom().nextInt(this.vendingItems.size())); } @@ -273,21 +270,23 @@ public class Item implements ISerialize { @Override public void serialize(ServerMessage message) { - message.appendString(this.type.code.toLowerCase()); + message.appendString(this.type == null ? "" : this.type.code.toLowerCase()); if (type == FurnitureType.BADGE) { - message.appendString(this.customParams); + message.appendString(ItemDataGuard.safeString(this.customParams)); } else { message.appendInt(this.spriteId); - if (this.getName().contains("wallpaper_single") || this.getName().contains("floor_single") || this.getName().contains("landscape_single")) { - message.appendString(this.name.split("_")[2]); + String itemName = ItemDataGuard.safeString(this.getName()); + if (itemName.contains("wallpaper_single") || itemName.contains("floor_single") || itemName.contains("landscape_single")) { + String[] nameParts = itemName.split("_"); + message.appendString(nameParts.length > 2 ? nameParts[2] : ""); } else if (type == FurnitureType.ROBOT) { - message.appendString(this.customParams); - } else if (name.equalsIgnoreCase("poster")) { - message.appendString(this.customParams); - } else if (name.startsWith("SONG ")) { - message.appendString(this.customParams); + message.appendString(ItemDataGuard.safeString(this.customParams)); + } else if (itemName.equalsIgnoreCase("poster")) { + message.appendString(ItemDataGuard.safeString(this.customParams)); + } else if (itemName.startsWith("SONG ")) { + message.appendString(ItemDataGuard.safeString(this.customParams)); } else { message.appendString(""); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemDataGuard.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemDataGuard.java new file mode 100644 index 00000000..446a55e8 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemDataGuard.java @@ -0,0 +1,82 @@ +package com.eu.habbo.habbohotel.items; + +final class ItemDataGuard { + static final int MAX_EXTRA_DATA_LENGTH = 1000; + + private ItemDataGuard() { + } + + static String safeString(String value) { + return value == null ? "" : value; + } + + static String normalizeExtraData(String value) { + String safe = safeString(value); + return safe.length() > MAX_EXTRA_DATA_LENGTH ? safe.substring(0, MAX_EXTRA_DATA_LENGTH) : safe; + } + + static int parsePositiveInt(String value) { + try { + int parsed = Integer.parseInt(safeString(value).trim()); + return parsed > 0 ? parsed : 0; + } catch (NumberFormatException e) { + return 0; + } + } + + static int[] parsePositiveIntList(String value) { + String safe = safeString(value).replace(";", ",").replace(".", ","); + if (safe.isBlank()) { + return new int[0]; + } + + String[] parts = safe.split(","); + int[] parsed = new int[parts.length]; + int count = 0; + + for (String part : parts) { + int id = parsePositiveInt(part); + if (id > 0) { + parsed[count++] = id; + } + } + + if (count == parsed.length) { + return parsed; + } + + int[] compact = new int[count]; + System.arraycopy(parsed, 0, compact, 0, count); + return compact; + } + + static double[] parseHeights(String value) { + String safe = safeString(value); + if (safe.isBlank() || !safe.contains(";")) { + return new double[0]; + } + + String[] parts = safe.split(";"); + double[] parsed = new double[parts.length]; + int count = 0; + + for (String part : parts) { + try { + double height = Double.parseDouble(part.trim()); + if (Double.isFinite(height)) { + parsed[count++] = height; + } + } catch (NumberFormatException e) { + // Ignore malformed DB values and keep the remaining heights usable. + } + } + + if (count == parsed.length) { + return parsed; + } + + double[] compact = new double[count]; + System.arraycopy(parsed, 0, compact, 0, count); + return compact; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java index 9280c25d..d2b40685 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/ItemManager.java @@ -566,6 +566,10 @@ public class ItemManager { public int calculateCrackState(int count, int max, Item baseItem) { + if (count <= 0 || max <= 0 || baseItem == null || baseItem.getStateCount() <= 0) { + return 0; + } + return (int) Math.floor((1.0D / ((double) max / (double) count) * baseItem.getStateCount())); } @@ -574,7 +578,8 @@ public class ItemManager { } public Item getCrackableReward(int itemId) { - return this.getItem(this.crackableRewards.get(itemId).getRandomReward()); + CrackableReward reward = this.crackableRewards.get(itemId); + return reward == null ? null : this.getItem(reward.getRandomReward()); } @@ -604,6 +609,12 @@ public class ItemManager { } public HabboItem createItem(int habboId, Item item, int limitedStack, int limitedSells, String extraData) { + if (habboId <= 0 || item == null) { + return null; + } + + extraData = ItemDataGuard.normalizeExtraData(extraData); + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO items (user_id, item_id, extra_data, limited_data) VALUES (?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { statement.setInt(1, habboId); statement.setInt(2, item.getId()); @@ -673,6 +684,12 @@ public class ItemManager { } public HabboItem handleRecycle(Habbo habbo, String itemId) { + int rewardItemId = ItemDataGuard.parsePositiveInt(itemId); + if (habbo == null || habbo.getHabboInfo() == null || rewardItemId <= 0 + || Emulator.getGameEnvironment().getCatalogManager().ecotronItem == null) { + return null; + } + String extradata = Calendar.getInstance().get(Calendar.DAY_OF_MONTH) + "-" + (Calendar.getInstance().get(Calendar.MONTH) + 1) + "-" + Calendar.getInstance().get(Calendar.YEAR); HabboItem item = null; @@ -686,7 +703,7 @@ public class ItemManager { try (PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO items_presents (item_id, base_item_reward) VALUES (?, ?)")) { while (set.next() && item == null) { preparedStatement.setInt(1, set.getInt(1)); - preparedStatement.setInt(2, Integer.parseInt(itemId)); + preparedStatement.setInt(2, rewardItemId); preparedStatement.addBatch(); item = new InteractionDefault(set.getInt(1), habbo.getHabboInfo().getId(), Emulator.getGameEnvironment().getCatalogManager().ecotronItem, extradata, 0, 0); } @@ -829,6 +846,10 @@ public class ItemManager { } public HabboItem createGift(String username, Item item, String extraData, int limitedStack, int limitedSells) { + if (username == null || username.isBlank() || item == null) { + return null; + } + Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(username); int userId = 0; @@ -857,13 +878,13 @@ public class ItemManager { } public HabboItem createGift(int userId, Item item, String extraData, int limitedStack, int limitedSells) { - if (userId == 0) + if (userId <= 0 || item == null) return null; - if (extraData.length() > 1000) { + if (extraData != null && extraData.length() > ItemDataGuard.MAX_EXTRA_DATA_LENGTH) { LOGGER.error("Extradata exceeds maximum length of 1000 characters: {}", extraData); - extraData = extraData.substring(0, 1000); } + extraData = ItemDataGuard.normalizeExtraData(extraData); HabboItem gift = this.createItem(userId, item, limitedStack, limitedSells, extraData); @@ -879,7 +900,7 @@ public class ItemManager { } public Item getItem(int itemId) { - if (itemId < 0) + if (itemId <= 0) return null; return this.items.get(itemId); @@ -890,12 +911,16 @@ public class ItemManager { } public Item getItem(String itemName) { + if (itemName == null || itemName.isBlank()) { + return null; + } + TIntObjectIterator item = this.items.iterator(); for (int i = this.items.size(); i-- > 0; ) { try { item.advance(); - if (item.value().getName().equalsIgnoreCase(itemName)) { + if (item.value() != null && item.value().getName() != null && item.value().getName().equalsIgnoreCase(itemName)) { return item.value(); } } catch (NoSuchElementException e) { diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/ItemDataGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/ItemDataGuardTest.java new file mode 100644 index 00000000..cb978fed --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/ItemDataGuardTest.java @@ -0,0 +1,26 @@ +package com.eu.habbo.habbohotel.items; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ItemDataGuardTest { + + @Test + void normalizesExtraDataToDatabaseBound() { + assertEquals("", ItemDataGuard.normalizeExtraData(null)); + assertEquals(ItemDataGuard.MAX_EXTRA_DATA_LENGTH, + ItemDataGuard.normalizeExtraData("x".repeat(ItemDataGuard.MAX_EXTRA_DATA_LENGTH + 1)).length()); + } + + @Test + void parsesOnlyPositiveVendingIds() { + assertArrayEquals(new int[]{1, 2, 3}, ItemDataGuard.parsePositiveIntList("1; 2.bad,3,-4,0")); + } + + @Test + void ignoresMalformedMultiHeights() { + assertArrayEquals(new double[]{0.5, 1.25}, ItemDataGuard.parseHeights("0.5;nope;Infinity;1.25")); + } +} From 240fede12a4e4d5ac8f5fdd8576c527e0d69bf1e Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 18:06:36 +0200 Subject: [PATCH 22/43] fix(wired): bound save payloads --- .../items/interactions/InteractionWired.java | 32 ++----- .../interactions/wired/WiredInputGuard.java | 90 +++++++++++++++++++ .../wired/WiredConditionSaveDataEvent.java | 8 +- .../wired/WiredEffectSaveDataEvent.java | 8 +- .../wired/WiredTriggerSaveDataEvent.java | 8 +- .../wired/WiredInputGuardTest.java | 51 +++++++++++ 6 files changed, 168 insertions(+), 29 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredInputGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWired.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWired.java index ffbf6996..170f56be 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWired.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/InteractionWired.java @@ -2,6 +2,7 @@ package com.eu.habbo.habbohotel.items.interactions; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.items.Item; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredInputGuard; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; @@ -230,39 +231,18 @@ public abstract class InteractionWired extends InteractionDefault { public static WiredSettings readSettings(ClientMessage packet, boolean isEffect) { - int intParamCount = packet.readInt(); - if (intParamCount < 0 || intParamCount > 100) { - throw new IllegalArgumentException("Invalid intParamCount: " + intParamCount); - } - int[] intParams = new int[intParamCount]; - - for(int i = 0; i < intParamCount; i++) - { - intParams[i] = packet.readInt(); - } - - String stringParam = packet.readString(); - - int itemCount = packet.readInt(); - int selectionLimit = Emulator.getConfig() != null ? Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5) : 5; - if (itemCount < 0 || itemCount > selectionLimit * 20) { - throw new IllegalArgumentException("Invalid itemCount: " + itemCount + " exceeds maximum allowed limit"); - } - int[] itemIds = new int[itemCount]; - - for(int i = 0; i < itemCount; i++) - { - itemIds[i] = packet.readInt(); - } + int[] intParams = WiredInputGuard.readIntParams(packet); + String stringParam = WiredInputGuard.readStringParam(packet); + int[] itemIds = WiredInputGuard.readFurniIds(packet); WiredSettings settings = new WiredSettings(intParams, stringParam, itemIds, -1); if(isEffect) { - settings.setDelay(packet.readInt()); + settings.setDelay(WiredInputGuard.normalizeDelay(packet.readInt())); } - settings.setStuffTypeSelectionCode(packet.readInt()); + settings.setStuffTypeSelectionCode(WiredInputGuard.normalizeStuffSelectionCode(packet.readInt())); return settings; } } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredInputGuard.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredInputGuard.java new file mode 100644 index 00000000..709133c6 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredInputGuard.java @@ -0,0 +1,90 @@ +package com.eu.habbo.habbohotel.items.interactions.wired; + +import com.eu.habbo.Emulator; +import com.eu.habbo.messages.ClientMessage; + +import java.util.Arrays; + +public final class WiredInputGuard { + public static final int MAX_INT_PARAMS = 100; + public static final int MAX_STRING_PARAM_LENGTH = 1024; + public static final int MAX_ABSOLUTE_FURNI_IDS = 100; + public static final int DEFAULT_MAX_DELAY = 20; + public static final int MAX_ABSOLUTE_DELAY = 3600; + public static final int MIN_STUFF_SELECTION_CODE = -1; + public static final int MAX_STUFF_SELECTION_CODE = 2; + + private WiredInputGuard() { + } + + public static int[] readIntParams(ClientMessage packet) { + int count = packet.readInt(); + if (count < 0 || count > MAX_INT_PARAMS) { + throw new IllegalArgumentException("Invalid wired int param count"); + } + + int[] values = new int[count]; + for (int i = 0; i < count; i++) { + values[i] = packet.readInt(); + } + return values; + } + + public static String readStringParam(ClientMessage packet) { + String value = packet.readString(); + if (value == null || value.isEmpty()) { + return ""; + } + + return value.length() > MAX_STRING_PARAM_LENGTH + ? value.substring(0, MAX_STRING_PARAM_LENGTH) + : value; + } + + public static int[] readFurniIds(ClientMessage packet) { + int count = packet.readInt(); + int maxCount = maxFurniSelectionCount(); + if (count < 0 || count > maxCount) { + throw new IllegalArgumentException("Invalid wired furni selection count"); + } + + int[] values = new int[count]; + int accepted = 0; + for (int i = 0; i < count; i++) { + int itemId = packet.readInt(); + if (itemId > 0) { + values[accepted++] = itemId; + } + } + + return accepted == values.length ? values : Arrays.copyOf(values, accepted); + } + + public static int normalizeDelay(int delay) { + return Math.max(0, Math.min(delay, maxDelay())); + } + + public static int normalizeStuffSelectionCode(int code) { + if (code < MIN_STUFF_SELECTION_CODE || code > MAX_STUFF_SELECTION_CODE) { + return MIN_STUFF_SELECTION_CODE; + } + + return code; + } + + public static int maxFurniSelectionCount() { + int selectionLimit = Emulator.getConfig() != null + ? Emulator.getConfig().getInt("hotel.wired.furni.selection.count", 5) + : 5; + selectionLimit = Math.max(1, selectionLimit); + return Math.min(MAX_ABSOLUTE_FURNI_IDS, selectionLimit * 20); + } + + public static int maxDelay() { + int configured = Emulator.getConfig() != null + ? Emulator.getConfig().getInt("hotel.wired.max_delay", DEFAULT_MAX_DELAY) + : DEFAULT_MAX_DELAY; + configured = Math.max(0, configured); + return Math.min(MAX_ABSOLUTE_DELAY, configured); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredConditionSaveDataEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredConditionSaveDataEvent.java index c1ca5c37..8efad3e8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredConditionSaveDataEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredConditionSaveDataEvent.java @@ -31,7 +31,13 @@ public class WiredConditionSaveDataEvent extends MessageHandler { if(saveMethod.isPresent()) { if (saveMethod.get().getParameterTypes()[0] == WiredSettings.class) { - WiredSettings settings = InteractionWired.readSettings(this.packet, false); + WiredSettings settings; + try { + settings = InteractionWired.readSettings(this.packet, false); + } catch (IllegalArgumentException e) { + this.client.sendResponse(new UpdateFailedComposer("Invalid wired condition settings")); + return; + } if (condition.saveData(settings)) { this.client.sendResponse(new WiredSavedComposer()); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java index 85728ba7..0ae07dc6 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredEffectSaveDataEvent.java @@ -28,7 +28,13 @@ public class WiredEffectSaveDataEvent extends MessageHandler { if (effect == null && extra == null) throw new WiredSaveException(String.format("Wired effect/extra with item id %s not found in room", itemId)); - WiredSettings settings = InteractionWired.readSettings(this.packet, true); + WiredSettings settings; + try { + settings = InteractionWired.readSettings(this.packet, true); + } catch (IllegalArgumentException e) { + this.client.sendResponse(new UpdateFailedComposer("Invalid wired effect settings")); + return; + } boolean saved; if (effect != null) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveDataEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveDataEvent.java index 66970dd2..0f503860 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveDataEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/wired/WiredTriggerSaveDataEvent.java @@ -22,7 +22,13 @@ public class WiredTriggerSaveDataEvent extends MessageHandler { InteractionWiredTrigger trigger = room.getRoomSpecialTypes().getTrigger(itemId); if (trigger != null) { - WiredSettings settings = InteractionWired.readSettings(this.packet, false); + WiredSettings settings; + try { + settings = InteractionWired.readSettings(this.packet, false); + } catch (IllegalArgumentException e) { + this.client.sendResponse(new UpdateFailedComposer("Invalid wired trigger settings")); + return; + } try { boolean saved = trigger.saveData(settings, this.client); diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredInputGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredInputGuardTest.java new file mode 100644 index 00000000..8341d9bc --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredInputGuardTest.java @@ -0,0 +1,51 @@ +package com.eu.habbo.habbohotel.items.interactions.wired; + +import com.eu.habbo.messages.ClientMessage; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WiredInputGuardTest { + + @Test + void trimsOversizedStringParams() { + String input = "x".repeat(WiredInputGuard.MAX_STRING_PARAM_LENGTH + 1); + ClientMessage message = new ClientMessage(1, stringBuffer(input)); + + assertEquals(WiredInputGuard.MAX_STRING_PARAM_LENGTH, + WiredInputGuard.readStringParam(message).length()); + } + + @Test + void filtersNonPositiveFurniIds() { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeInt(4); + buffer.writeInt(1); + buffer.writeInt(0); + buffer.writeInt(-1); + buffer.writeInt(2); + + assertArrayEquals(new int[]{1, 2}, WiredInputGuard.readFurniIds(new ClientMessage(1, buffer))); + } + + @Test + void clampsDelayAndSelectionCode() { + assertEquals(0, WiredInputGuard.normalizeDelay(-10)); + assertEquals(WiredInputGuard.DEFAULT_MAX_DELAY, WiredInputGuard.normalizeDelay(WiredInputGuard.DEFAULT_MAX_DELAY + 1)); + assertEquals(-1, WiredInputGuard.normalizeStuffSelectionCode(99)); + assertEquals(2, WiredInputGuard.normalizeStuffSelectionCode(2)); + } + + private static ByteBuf stringBuffer(String value) { + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + ByteBuf buffer = Unpooled.buffer(); + buffer.writeShort(bytes.length); + buffer.writeBytes(bytes); + return buffer; + } +} From 2f46f316843c8e05683235920b581762016ab6b8 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 18:11:08 +0200 Subject: [PATCH 23/43] fix(wired): bound reward amounts --- .../wired/WiredNumericInputGuard.java | 38 +++++++++++++++++++ ...redEffectGiveHotelviewBonusRarePoints.java | 7 ++-- .../WiredEffectGiveHotelviewHofPoints.java | 7 ++-- .../wired/effects/WiredEffectGiveRespect.java | 7 ++-- .../wired/WiredNumericInputGuardTest.java | 28 ++++++++++++++ 5 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredNumericInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredNumericInputGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredNumericInputGuard.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredNumericInputGuard.java new file mode 100644 index 00000000..d06f216f --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredNumericInputGuard.java @@ -0,0 +1,38 @@ +package com.eu.habbo.habbohotel.items.interactions.wired; + +import com.eu.habbo.Emulator; + +public final class WiredNumericInputGuard { + public static final int DEFAULT_MAX_REWARD_AMOUNT = 1000; + public static final int DEFAULT_MAX_RESPECT_AMOUNT = 100; + public static final int MAX_ABSOLUTE_AMOUNT = 100000; + + private WiredNumericInputGuard() { + } + + public static int parsePositiveAmount(String value, int maxAmount) { + try { + int parsed = Integer.parseInt(value == null ? "" : value.trim()); + if (parsed <= 0) { + return 0; + } + + return Math.min(parsed, Math.max(1, Math.min(maxAmount, MAX_ABSOLUTE_AMOUNT))); + } catch (NumberFormatException e) { + return 0; + } + } + + public static int maxRewardAmount() { + return configuredMax("hotel.wired.reward.max_amount", DEFAULT_MAX_REWARD_AMOUNT); + } + + public static int maxRespectAmount() { + return configuredMax("hotel.wired.respect.max_amount", DEFAULT_MAX_RESPECT_AMOUNT); + } + + private static int configuredMax(String key, int fallback) { + int configured = Emulator.getConfig() != null ? Emulator.getConfig().getInt(key, fallback) : fallback; + return Math.max(1, Math.min(configured, MAX_ABSOLUTE_AMOUNT)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveHotelviewBonusRarePoints.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveHotelviewBonusRarePoints.java index f33d591a..b9b66356 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveHotelviewBonusRarePoints.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveHotelviewBonusRarePoints.java @@ -5,6 +5,7 @@ import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredNumericInputGuard; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; @@ -72,11 +73,11 @@ public class WiredEffectGiveHotelviewBonusRarePoints extends InteractionWiredEff @Override public boolean saveData(WiredSettings settings, GameClient gameClient) { - try { - this.amount = Integer.parseInt(settings.getStringParam()); - } catch (Exception e) { + int nextAmount = WiredNumericInputGuard.parsePositiveAmount(settings.getStringParam(), WiredNumericInputGuard.maxRewardAmount()); + if (nextAmount <= 0) { return false; } + this.amount = nextAmount; int[] params = settings.getIntParams(); this.userSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveHotelviewHofPoints.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveHotelviewHofPoints.java index 6229acbf..f38ffacd 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveHotelviewHofPoints.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveHotelviewHofPoints.java @@ -5,6 +5,7 @@ import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredNumericInputGuard; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; @@ -71,11 +72,11 @@ public class WiredEffectGiveHotelviewHofPoints extends InteractionWiredEffect { @Override public boolean saveData(WiredSettings settings, GameClient gameClient) { - try { - this.amount = Integer.parseInt(settings.getStringParam()); - } catch (Exception e) { + int nextAmount = WiredNumericInputGuard.parsePositiveAmount(settings.getStringParam(), WiredNumericInputGuard.maxRewardAmount()); + if (nextAmount <= 0) { return false; } + this.amount = nextAmount; int[] params = settings.getIntParams(); this.userSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveRespect.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveRespect.java index 335792d6..a01951b2 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveRespect.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectGiveRespect.java @@ -6,6 +6,7 @@ import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredNumericInputGuard; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; @@ -72,11 +73,11 @@ public class WiredEffectGiveRespect extends InteractionWiredEffect { @Override public boolean saveData(WiredSettings settings, GameClient gameClient) { - try { - this.respects = Integer.parseInt(settings.getStringParam()); - } catch (Exception e) { + int nextRespects = WiredNumericInputGuard.parsePositiveAmount(settings.getStringParam(), WiredNumericInputGuard.maxRespectAmount()); + if (nextRespects <= 0) { return false; } + this.respects = nextRespects; int[] params = settings.getIntParams(); this.userSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredNumericInputGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredNumericInputGuardTest.java new file mode 100644 index 00000000..33e5127b --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredNumericInputGuardTest.java @@ -0,0 +1,28 @@ +package com.eu.habbo.habbohotel.items.interactions.wired; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WiredNumericInputGuardTest { + + @Test + void rejectsInvalidOrNonPositiveAmounts() { + assertEquals(0, WiredNumericInputGuard.parsePositiveAmount(null, 100)); + assertEquals(0, WiredNumericInputGuard.parsePositiveAmount("nope", 100)); + assertEquals(0, WiredNumericInputGuard.parsePositiveAmount("0", 100)); + assertEquals(0, WiredNumericInputGuard.parsePositiveAmount("-5", 100)); + } + + @Test + void clampsAmountsToConfiguredMaximum() { + assertEquals(50, WiredNumericInputGuard.parsePositiveAmount("50", 100)); + assertEquals(100, WiredNumericInputGuard.parsePositiveAmount("500", 100)); + } + + @Test + void appliesAbsoluteMaximumEvenWhenConfiguredTooHigh() { + assertEquals(WiredNumericInputGuard.MAX_ABSOLUTE_AMOUNT, + WiredNumericInputGuard.parsePositiveAmount("999999999", Integer.MAX_VALUE)); + } +} From 0ceeb5ca702e9241c4361ce5a2e85097713c2981 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 18:15:08 +0200 Subject: [PATCH 24/43] fix(wired): tolerate legacy furni data --- .../wired/WiredLegacyDataGuard.java | 48 +++++++++++++++++++ .../wired/effects/WiredEffectTeleport.java | 10 ++-- .../wired/effects/WiredEffectToggleFurni.java | 10 ++-- .../wired/WiredLegacyDataGuardTest.java | 26 ++++++++++ 4 files changed, 81 insertions(+), 13 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredLegacyDataGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredLegacyDataGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredLegacyDataGuard.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredLegacyDataGuard.java new file mode 100644 index 00000000..4ec20d49 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredLegacyDataGuard.java @@ -0,0 +1,48 @@ +package com.eu.habbo.habbohotel.items.interactions.wired; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.users.HabboItem; + +import java.util.ArrayList; +import java.util.List; + +public final class WiredLegacyDataGuard { + public static final int DEFAULT_MAX_DELAY = 20; + + private WiredLegacyDataGuard() { + } + + public static int parseDelay(String value) { + try { + int parsed = Integer.parseInt(value == null ? "" : value.trim()); + return Math.max(0, Math.min(parsed, DEFAULT_MAX_DELAY)); + } catch (NumberFormatException e) { + return 0; + } + } + + public static List parseRoomItems(String value, Room room) { + List items = new ArrayList<>(); + if (room == null || value == null || value.isBlank()) { + return items; + } + + for (String part : value.split(";")) { + try { + int itemId = Integer.parseInt(part.trim()); + if (itemId <= 0) { + continue; + } + + HabboItem item = room.getHabboItem(itemId); + if (item != null) { + items.add(item); + } + } catch (NumberFormatException e) { + // Ignore malformed legacy ids and keep loading the remaining items. + } + } + + return items; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTeleport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTeleport.java index 6a5e4b59..3c0207d1 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTeleport.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectTeleport.java @@ -5,6 +5,7 @@ import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredLegacyDataGuard; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.pets.RideablePet; import com.eu.habbo.habbohotel.rooms.*; @@ -287,16 +288,11 @@ public class WiredEffectTeleport extends InteractionWiredEffect { String[] wiredDataOld = wiredData.split("\t"); if (wiredDataOld.length >= 1) { - this.setDelay(Integer.parseInt(wiredDataOld[0])); + this.setDelay(WiredLegacyDataGuard.parseDelay(wiredDataOld[0])); } if (wiredDataOld.length == 2) { if (wiredDataOld[1].contains(";")) { - for (String s : wiredDataOld[1].split(";")) { - HabboItem item = room.getHabboItem(Integer.parseInt(s)); - - if (item != null) - this.items.add(item); - } + this.items.addAll(WiredLegacyDataGuard.parseRoomItems(wiredDataOld[1], room)); } } this.fastTeleport = false; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectToggleFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectToggleFurni.java index 6aa3a078..19d9fd3d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectToggleFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectToggleFurni.java @@ -15,6 +15,7 @@ import com.eu.habbo.habbohotel.items.interactions.games.freeze.InteractionFreeze import com.eu.habbo.habbohotel.items.interactions.games.tag.InteractionTagField; import com.eu.habbo.habbohotel.items.interactions.games.tag.InteractionTagPole; import com.eu.habbo.habbohotel.items.interactions.pets.*; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredLegacyDataGuard; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; @@ -274,18 +275,15 @@ public class WiredEffectToggleFurni extends InteractionWiredEffect { String[] wiredDataOld = wiredData.split("\t"); if (wiredDataOld.length >= 1) { - this.setDelay(Integer.parseInt(wiredDataOld[0])); + this.setDelay(WiredLegacyDataGuard.parseDelay(wiredDataOld[0])); } if (wiredDataOld.length == 2) { if (wiredDataOld[1].contains(";")) { - for (String s : wiredDataOld[1].split(";")) { - HabboItem item = room.getHabboItem(Integer.parseInt(s)); - + for (HabboItem item : WiredLegacyDataGuard.parseRoomItems(wiredDataOld[1], room)) { if (item instanceof InteractionFreezeBlock || item instanceof InteractionFreezeTile || item instanceof InteractionCrackable) continue; - if (item != null) - this.items.add(item); + this.items.add(item); } } } diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredLegacyDataGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredLegacyDataGuardTest.java new file mode 100644 index 00000000..4f939997 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredLegacyDataGuardTest.java @@ -0,0 +1,26 @@ +package com.eu.habbo.habbohotel.items.interactions.wired; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WiredLegacyDataGuardTest { + + @Test + void malformedDelayFallsBackToZero() { + assertEquals(0, WiredLegacyDataGuard.parseDelay(null)); + assertEquals(0, WiredLegacyDataGuard.parseDelay("nope")); + assertEquals(0, WiredLegacyDataGuard.parseDelay("-5")); + } + + @Test + void oversizedDelayIsClampedThroughWiredInputGuard() { + assertEquals(WiredLegacyDataGuard.DEFAULT_MAX_DELAY, WiredLegacyDataGuard.parseDelay("999999")); + } + + @Test + void nullRoomOrBlankItemsReturnEmptyList() { + assertEquals(0, WiredLegacyDataGuard.parseRoomItems("1;2;bad", null).size()); + assertEquals(0, WiredLegacyDataGuard.parseRoomItems("", null).size()); + } +} From 916b9df7a3295ec9c6976b8562b04b787054bed5 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 18:20:55 +0200 Subject: [PATCH 25/43] fix(wired): clamp trigger timers --- .../wired/WiredTimerInputGuard.java | 45 +++++++++++++++++++ .../wired/triggers/WiredTriggerAtSetTime.java | 25 ++++++----- .../triggers/WiredTriggerAtTimeLong.java | 25 ++++++----- .../wired/triggers/WiredTriggerRepeater.java | 31 +++++++------ .../triggers/WiredTriggerRepeaterLong.java | 29 ++++++------ .../triggers/WiredTriggerRepeaterShort.java | 4 +- .../wired/WiredTimerInputGuardTest.java | 35 +++++++++++++++ 7 files changed, 142 insertions(+), 52 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredTimerInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredTimerInputGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredTimerInputGuard.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredTimerInputGuard.java new file mode 100644 index 00000000..e47e7850 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredTimerInputGuard.java @@ -0,0 +1,45 @@ +package com.eu.habbo.habbohotel.items.interactions.wired; + +public final class WiredTimerInputGuard { + public static final int MAX_TIMER_MS = 24 * 60 * 60 * 1000; + + private WiredTimerInputGuard() { + } + + public static int fromClientUnits(int units, int stepMs, int minMs) { + return fromClientUnits(units, stepMs, minMs, MAX_TIMER_MS); + } + + public static int fromClientUnits(int units, int stepMs, int minMs, int maxMs) { + if (units < 1 || stepMs < 1) { + return minMs; + } + + long value = (long) units * stepMs; + return clamp(value, minMs, maxMs); + } + + public static int normalizeStoredMillis(Integer millis, int minMs, int fallbackMs) { + return normalizeStoredMillis(millis, minMs, fallbackMs, MAX_TIMER_MS); + } + + public static int normalizeStoredMillis(Integer millis, int minMs, int fallbackMs, int maxMs) { + if (millis == null || millis < minMs) { + return fallbackMs; + } + + return clamp(millis.longValue(), minMs, maxMs); + } + + private static int clamp(long value, int minMs, int maxMs) { + if (value < minMs) { + return minMs; + } + + if (value > maxMs) { + return maxMs; + } + + return (int) value; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtSetTime.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtSetTime.java index 9f460464..c9a02f60 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtSetTime.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtSetTime.java @@ -4,6 +4,7 @@ import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredTimerInputGuard; import com.eu.habbo.habbohotel.items.interactions.wired.WiredTriggerReset; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; @@ -29,6 +30,9 @@ import java.util.List; */ public class WiredTriggerAtSetTime extends InteractionWiredTrigger implements WiredTickable, WiredTriggerReset { public static final WiredTriggerType type = WiredTriggerType.AT_GIVEN_TIME; + private static final int STEP_MS = 500; + private static final int MIN_DELAY = STEP_MS; + private static final int LEGACY_FALLBACK_DELAY = 20 * STEP_MS; /** The time in milliseconds until the trigger fires */ public int executeTime; @@ -68,18 +72,19 @@ public class WiredTriggerAtSetTime extends InteractionWiredTrigger implements Wi public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); - if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.executeTime = data.executeTime; - } else { - if (wiredData.length() >= 1) { - this.executeTime = (Integer.parseInt(wiredData)); + Integer storedExecuteTime = null; + try { + if (wiredData != null && wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + storedExecuteTime = data != null ? data.executeTime : null; + } else if (wiredData != null && wiredData.length() >= 1) { + storedExecuteTime = Integer.parseInt(wiredData); } + } catch (RuntimeException ignored) { + storedExecuteTime = null; } - if (this.executeTime < 500) { - this.executeTime = 20 * 500; - } + this.executeTime = WiredTimerInputGuard.normalizeStoredMillis(storedExecuteTime, MIN_DELAY, LEGACY_FALLBACK_DELAY); // Initialize for tick system - will be registered by RoomItemManager this.accumulatedTime = 0; @@ -134,7 +139,7 @@ public class WiredTriggerAtSetTime extends InteractionWiredTrigger implements Wi @Override public boolean saveData(WiredSettings settings) { if (settings.getIntParams().length < 1) return false; - this.executeTime = settings.getIntParams()[0] * 500; + this.executeTime = WiredTimerInputGuard.fromClientUnits(settings.getIntParams()[0], STEP_MS, MIN_DELAY); this.resetTimer(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtTimeLong.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtTimeLong.java index 8864f4c3..1ddadc50 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtTimeLong.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtTimeLong.java @@ -4,6 +4,7 @@ import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredTimerInputGuard; import com.eu.habbo.habbohotel.items.interactions.wired.WiredTriggerReset; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; @@ -29,6 +30,9 @@ import java.util.List; */ public class WiredTriggerAtTimeLong extends InteractionWiredTrigger implements WiredTickable, WiredTriggerReset { private static final WiredTriggerType type = WiredTriggerType.AT_GIVEN_TIME; + private static final int STEP_MS = 500; + private static final int MIN_DELAY = STEP_MS; + private static final int LEGACY_FALLBACK_DELAY = 20 * STEP_MS; /** The time in milliseconds until the trigger fires */ private int executeTime; @@ -68,18 +72,19 @@ public class WiredTriggerAtTimeLong extends InteractionWiredTrigger implements W public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); - if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.executeTime = data.executeTime; - } else { - if (wiredData.length() >= 1) { - this.executeTime = (Integer.parseInt(wiredData)); + Integer storedExecuteTime = null; + try { + if (wiredData != null && wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + storedExecuteTime = data != null ? data.executeTime : null; + } else if (wiredData != null && wiredData.length() >= 1) { + storedExecuteTime = Integer.parseInt(wiredData); } + } catch (RuntimeException ignored) { + storedExecuteTime = null; } - if (this.executeTime < 500) { - this.executeTime = 20 * 500; - } + this.executeTime = WiredTimerInputGuard.normalizeStoredMillis(storedExecuteTime, MIN_DELAY, LEGACY_FALLBACK_DELAY); // Initialize for tick system this.accumulatedTime = 0; @@ -134,7 +139,7 @@ public class WiredTriggerAtTimeLong extends InteractionWiredTrigger implements W @Override public boolean saveData(WiredSettings settings) { if (settings.getIntParams().length < 1) return false; - this.executeTime = settings.getIntParams()[0] * 500; + this.executeTime = WiredTimerInputGuard.fromClientUnits(settings.getIntParams()[0], STEP_MS, MIN_DELAY); this.resetTimer(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeater.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeater.java index 2ac70e88..2be1b2ff 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeater.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeater.java @@ -4,6 +4,7 @@ import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredTimerInputGuard; import com.eu.habbo.habbohotel.items.interactions.wired.WiredTriggerReset; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; @@ -30,6 +31,9 @@ import java.util.List; public class WiredTriggerRepeater extends InteractionWiredTrigger implements WiredTickable, WiredTriggerReset { public static final WiredTriggerType type = WiredTriggerType.PERIODICALLY; public static final int DEFAULT_DELAY = 10 * 500; // 5 seconds default + private static final int STEP_MS = 500; + private static final int MIN_DELAY = STEP_MS; + private static final int LEGACY_FALLBACK_DELAY = 20 * STEP_MS; /** The interval in milliseconds between triggers */ protected int repeatTime = DEFAULT_DELAY; @@ -63,18 +67,19 @@ public class WiredTriggerRepeater extends InteractionWiredTrigger implements Wir public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); - if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.repeatTime = data.repeatTime; - } else { - if (wiredData.length() >= 1) { - this.repeatTime = (Integer.parseInt(wiredData)); + Integer storedRepeatTime = null; + try { + if (wiredData != null && wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + storedRepeatTime = data != null ? data.repeatTime : null; + } else if (wiredData != null && wiredData.length() >= 1) { + storedRepeatTime = Integer.parseInt(wiredData); } + } catch (RuntimeException ignored) { + storedRepeatTime = null; } - if (this.repeatTime < 500) { - this.repeatTime = 20 * 500; - } + this.repeatTime = WiredTimerInputGuard.normalizeStoredMillis(storedRepeatTime, MIN_DELAY, LEGACY_FALLBACK_DELAY); } @Override @@ -123,13 +128,7 @@ public class WiredTriggerRepeater extends InteractionWiredTrigger implements Wir @Override public boolean saveData(WiredSettings settings) { if (settings.getIntParams().length < 1) return false; - int newRepeatTime = settings.getIntParams()[0] * 500; - - if (newRepeatTime < 500) { - newRepeatTime = 500; - } - - this.repeatTime = newRepeatTime; + this.repeatTime = WiredTimerInputGuard.fromClientUnits(settings.getIntParams()[0], STEP_MS, MIN_DELAY); return true; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterLong.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterLong.java index ffc1d92f..5e42f57b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterLong.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterLong.java @@ -4,6 +4,7 @@ import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredTrigger; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredTimerInputGuard; import com.eu.habbo.habbohotel.items.interactions.wired.WiredTriggerReset; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; @@ -29,6 +30,9 @@ import java.util.List; */ public class WiredTriggerRepeaterLong extends InteractionWiredTrigger implements WiredTickable, WiredTriggerReset { public static final int DEFAULT_DELAY = 10 * 5000; // 50 seconds default + private static final int STEP_MS = 5000; + private static final int MIN_DELAY = STEP_MS; + private static final int LEGACY_FALLBACK_DELAY = 20 * STEP_MS; private static final WiredTriggerType type = WiredTriggerType.PERIODICALLY_LONG; /** The interval in milliseconds between triggers */ @@ -63,18 +67,19 @@ public class WiredTriggerRepeaterLong extends InteractionWiredTrigger implements public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); - if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.repeatTime = data.repeatTime; - } else { - if (wiredData.length() >= 1) { - this.repeatTime = (Integer.parseInt(wiredData)); + Integer storedRepeatTime = null; + try { + if (wiredData != null && wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + storedRepeatTime = data != null ? data.repeatTime : null; + } else if (wiredData != null && wiredData.length() >= 1) { + storedRepeatTime = Integer.parseInt(wiredData); } + } catch (RuntimeException ignored) { + storedRepeatTime = null; } - if (this.repeatTime < 5000) { - this.repeatTime = 20 * 5000; - } + this.repeatTime = WiredTimerInputGuard.normalizeStoredMillis(storedRepeatTime, MIN_DELAY, LEGACY_FALLBACK_DELAY); } @Override @@ -123,11 +128,7 @@ public class WiredTriggerRepeaterLong extends InteractionWiredTrigger implements @Override public boolean saveData(WiredSettings settings) { if (settings.getIntParams().length < 1) return false; - int interval = settings.getIntParams()[0]; - if (interval < 1) { - interval = 1; - } - this.repeatTime = interval * 5000; + this.repeatTime = WiredTimerInputGuard.fromClientUnits(settings.getIntParams()[0], STEP_MS, MIN_DELAY); // No accumulated time reset needed - using global tick count return true; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterShort.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterShort.java index e20f5bae..40da9fc7 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterShort.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterShort.java @@ -3,6 +3,7 @@ package com.eu.habbo.habbohotel.items.interactions.wired.triggers; import com.eu.habbo.habbohotel.items.Item; import com.eu.habbo.habbohotel.items.interactions.InteractionWiredEffect; import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; +import com.eu.habbo.habbohotel.items.interactions.wired.WiredTimerInputGuard; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.wired.WiredTriggerType; import com.eu.habbo.habbohotel.wired.core.WiredManager; @@ -94,8 +95,7 @@ public class WiredTriggerRepeaterShort extends WiredTriggerRepeater { public boolean saveData(WiredSettings settings) { if (settings.getIntParams().length < 1) return false; - int newRepeatTime = settings.getIntParams()[0] * STEP_MS; - this.repeatTime = clampRepeatTime(newRepeatTime); + this.repeatTime = WiredTimerInputGuard.fromClientUnits(settings.getIntParams()[0], STEP_MS, MIN_DELAY, MAX_DELAY); return true; } diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredTimerInputGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredTimerInputGuardTest.java new file mode 100644 index 00000000..f739d781 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/WiredTimerInputGuardTest.java @@ -0,0 +1,35 @@ +package com.eu.habbo.habbohotel.items.interactions.wired; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WiredTimerInputGuardTest { + + @Test + void clientTimerUnitsAreMultipliedWithoutOverflow() { + assertEquals(500, WiredTimerInputGuard.fromClientUnits(1, 500, 500)); + assertEquals(WiredTimerInputGuard.MAX_TIMER_MS, + WiredTimerInputGuard.fromClientUnits(Integer.MAX_VALUE, 5000, 5000)); + } + + @Test + void invalidClientTimerUnitsUseMinimumDelay() { + assertEquals(500, WiredTimerInputGuard.fromClientUnits(0, 500, 500)); + assertEquals(5000, WiredTimerInputGuard.fromClientUnits(-10, 5000, 5000)); + } + + @Test + void storedTimerValuesFallbackOrClamp() { + assertEquals(10000, WiredTimerInputGuard.normalizeStoredMillis(null, 500, 10000)); + assertEquals(10000, WiredTimerInputGuard.normalizeStoredMillis(-1, 500, 10000)); + assertEquals(500, WiredTimerInputGuard.normalizeStoredMillis(500, 500, 10000)); + assertEquals(WiredTimerInputGuard.MAX_TIMER_MS, + WiredTimerInputGuard.normalizeStoredMillis(Integer.MAX_VALUE, 500, 10000)); + } + + @Test + void shortRepeaterKeepsItsLegacyMaximum() { + assertEquals(500, WiredTimerInputGuard.fromClientUnits(Integer.MAX_VALUE, 50, 50, 500)); + } +} From 7869938d98c5a815473706e5c18a6a935ff9b171 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 18:27:36 +0200 Subject: [PATCH 26/43] fix(wired): bound condition inputs --- .../conditions/WiredConditionHabboCount.java | 19 ++++--- .../conditions/WiredConditionInputGuard.java | 57 +++++++++++++++++++ .../WiredConditionLessTimeElapsed.java | 6 +- .../WiredConditionMoreTimeElapsed.java | 6 +- .../WiredConditionNotHabboCount.java | 23 ++++---- .../conditions/WiredConditionTeamMember.java | 10 ++-- .../WiredConditionInputGuardTest.java | 41 +++++++++++++ 7 files changed, 133 insertions(+), 29 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionInputGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboCount.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboCount.java index 98010557..9781565d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboCount.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboCount.java @@ -59,16 +59,14 @@ public class WiredConditionHabboCount extends InteractionWiredCondition { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.lowerLimit = data.lowerLimit; - this.upperLimit = data.upperLimit; - this.userSource = data.userSource; + this.applyLimits(data.lowerLimit, data.upperLimit); + this.userSource = WiredConditionInputGuard.normalizeUserSource(data.userSource); } else { String[] data = wiredData.split(":"); if (data.length >= 2) { try { - this.lowerLimit = Integer.parseInt(data[0].trim()); - this.upperLimit = Integer.parseInt(data[1].trim()); + this.applyLimits(Integer.parseInt(data[0].trim()), Integer.parseInt(data[1].trim())); } catch (NumberFormatException ignored) { // malformed legacy data — keep the constructed defaults } @@ -110,14 +108,19 @@ public class WiredConditionHabboCount extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { if(settings.getIntParams().length < 2) return false; - this.lowerLimit = settings.getIntParams()[0]; - this.upperLimit = settings.getIntParams()[1]; int[] params = settings.getIntParams(); - this.userSource = (params.length > 2) ? params[2] : WiredSourceUtil.SOURCE_TRIGGER; + this.applyLimits(params[0], params[1]); + this.userSource = (params.length > 2) ? WiredConditionInputGuard.normalizeUserSource(params[2]) : WiredSourceUtil.SOURCE_TRIGGER; return true; } + private void applyLimits(int lowerLimit, int upperLimit) { + int[] limits = WiredConditionInputGuard.normalizeUserCountRange(lowerLimit, upperLimit); + this.lowerLimit = limits[0]; + this.upperLimit = limits[1]; + } + static class JsonData { int lowerLimit; int upperLimit; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionInputGuard.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionInputGuard.java new file mode 100644 index 00000000..4d9e3df5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionInputGuard.java @@ -0,0 +1,57 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; + +public final class WiredConditionInputGuard { + public static final int MAX_USER_COUNT_LIMIT = 1000; + public static final int MAX_TIMER_CYCLES = 24 * 60 * 60 * 2; + + private WiredConditionInputGuard() { + } + + public static GameTeamColors normalizeTeamColor(GameTeamColors value, GameTeamColors fallback) { + return (value != null) ? value : fallback; + } + + public static GameTeamColors normalizeTeamColorType(int value, GameTeamColors fallback) { + for (GameTeamColors color : GameTeamColors.values()) { + if (color.type == value) { + return color; + } + } + + return fallback; + } + + public static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + public static int normalizeTimerCycles(int value) { + if (value < 0) { + return 0; + } + + return Math.min(value, MAX_TIMER_CYCLES); + } + + public static int[] normalizeUserCountRange(int lowerLimit, int upperLimit) { + int lower = clampUserCount(lowerLimit); + int upper = clampUserCount(upperLimit); + + if (lower > upper) { + return new int[]{upper, lower}; + } + + return new int[]{lower, upper}; + } + + private static int clampUserCount(int value) { + if (value < 0) { + return 0; + } + + return Math.min(value, MAX_USER_COUNT_LIMIT); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionLessTimeElapsed.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionLessTimeElapsed.java index 7086ea7d..7d294214 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionLessTimeElapsed.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionLessTimeElapsed.java @@ -52,10 +52,10 @@ public class WiredConditionLessTimeElapsed extends InteractionWiredCondition { try { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.cycles = data.cycles; + this.cycles = WiredConditionInputGuard.normalizeTimerCycles(data.cycles); } else { if (!wiredData.equals("")) - this.cycles = Integer.parseInt(wiredData); + this.cycles = WiredConditionInputGuard.normalizeTimerCycles(Integer.parseInt(wiredData)); } } catch (Exception e) { } @@ -90,7 +90,7 @@ public class WiredConditionLessTimeElapsed extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { if(settings.getIntParams().length < 1) return false; - this.cycles = settings.getIntParams()[0]; + this.cycles = WiredConditionInputGuard.normalizeTimerCycles(settings.getIntParams()[0]); return true; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMoreTimeElapsed.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMoreTimeElapsed.java index 2ae00378..865d2af0 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMoreTimeElapsed.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMoreTimeElapsed.java @@ -52,10 +52,10 @@ public class WiredConditionMoreTimeElapsed extends InteractionWiredCondition { try { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.cycles = data.cycles; + this.cycles = WiredConditionInputGuard.normalizeTimerCycles(data.cycles); } else { if (!wiredData.equals("")) - this.cycles = Integer.parseInt(wiredData); + this.cycles = WiredConditionInputGuard.normalizeTimerCycles(Integer.parseInt(wiredData)); } } catch (Exception e) { } @@ -90,7 +90,7 @@ public class WiredConditionMoreTimeElapsed extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { if(settings.getIntParams().length < 1) return false; - this.cycles = settings.getIntParams()[0]; + this.cycles = WiredConditionInputGuard.normalizeTimerCycles(settings.getIntParams()[0]); return true; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboCount.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboCount.java index e993d19f..f6b6ff2e 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboCount.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboCount.java @@ -59,15 +59,13 @@ public class WiredConditionNotHabboCount extends InteractionWiredCondition { if (wiredData.startsWith("{")) { WiredConditionHabboCount.JsonData data = WiredManager.getGson().fromJson(wiredData, WiredConditionHabboCount.JsonData.class); - this.lowerLimit = data.lowerLimit; - this.upperLimit = data.upperLimit; - this.userSource = data.userSource; + this.applyLimits(data.lowerLimit, data.upperLimit); + this.userSource = WiredConditionInputGuard.normalizeUserSource(data.userSource); } else { String[] data = wiredData.split(":"); if (data.length >= 2) { try { - this.lowerLimit = Integer.parseInt(data[0].trim()); - this.upperLimit = Integer.parseInt(data[1].trim()); + this.applyLimits(Integer.parseInt(data[0].trim()), Integer.parseInt(data[1].trim())); } catch (NumberFormatException ignored) { // malformed legacy data — keep the constructed defaults } @@ -78,8 +76,8 @@ public class WiredConditionNotHabboCount extends InteractionWiredCondition { @Override public void onPickUp() { - this.upperLimit = 0; - this.lowerLimit = 20; + this.lowerLimit = 10; + this.upperLimit = 20; this.userSource = WiredSourceUtil.SOURCE_TRIGGER; } @@ -109,14 +107,19 @@ public class WiredConditionNotHabboCount extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { if(settings.getIntParams().length < 2) return false; - this.lowerLimit = settings.getIntParams()[0]; - this.upperLimit = settings.getIntParams()[1]; int[] params = settings.getIntParams(); - this.userSource = (params.length > 2) ? params[2] : WiredSourceUtil.SOURCE_TRIGGER; + this.applyLimits(params[0], params[1]); + this.userSource = (params.length > 2) ? WiredConditionInputGuard.normalizeUserSource(params[2]) : WiredSourceUtil.SOURCE_TRIGGER; return true; } + private void applyLimits(int lowerLimit, int upperLimit) { + int[] limits = WiredConditionInputGuard.normalizeUserCountRange(lowerLimit, upperLimit); + this.lowerLimit = limits[0]; + this.upperLimit = limits[1]; + } + static class JsonData { int lowerLimit; int upperLimit; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamMember.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamMember.java index 4ca7b4e1..7707c680 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamMember.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamMember.java @@ -97,12 +97,12 @@ public class WiredConditionTeamMember extends InteractionWiredCondition { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.teamColor = data.teamColor; - this.userSource = data.userSource; + this.teamColor = WiredConditionInputGuard.normalizeTeamColor(data.teamColor, GameTeamColors.RED); + this.userSource = WiredConditionInputGuard.normalizeUserSource(data.userSource); this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY); } else { if (!wiredData.equals("")) - this.teamColor = GameTeamColors.values()[Integer.parseInt(wiredData)]; + this.teamColor = WiredConditionInputGuard.normalizeTeamColorType(Integer.parseInt(wiredData), GameTeamColors.RED); this.userSource = WiredSourceUtil.SOURCE_TRIGGER; this.quantifier = QUANTIFIER_ANY; } @@ -147,8 +147,8 @@ public class WiredConditionTeamMember extends InteractionWiredCondition { public boolean saveData(WiredSettings settings) { if(settings.getIntParams().length < 1) return false; int[] params = settings.getIntParams(); - this.teamColor = GameTeamColors.values()[params[0]]; - this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + this.teamColor = WiredConditionInputGuard.normalizeTeamColorType(params[0], GameTeamColors.RED); + this.userSource = (params.length > 1) ? WiredConditionInputGuard.normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER; this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2], QUANTIFIER_ALL) : QUANTIFIER_ANY; return true; diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionInputGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionInputGuardTest.java new file mode 100644 index 00000000..0d9f6747 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionInputGuardTest.java @@ -0,0 +1,41 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.games.GameTeamColors; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WiredConditionInputGuardTest { + + @Test + void teamColorsAreResolvedByProtocolTypeWithoutArrayIndexing() { + assertEquals(GameTeamColors.RED, WiredConditionInputGuard.normalizeTeamColorType(1, GameTeamColors.RED)); + assertEquals(GameTeamColors.TEN, WiredConditionInputGuard.normalizeTeamColorType(14, GameTeamColors.RED)); + assertEquals(GameTeamColors.RED, WiredConditionInputGuard.normalizeTeamColorType(-1, GameTeamColors.RED)); + assertEquals(GameTeamColors.RED, WiredConditionInputGuard.normalizeTeamColorType(Integer.MAX_VALUE, GameTeamColors.RED)); + } + + @Test + void userSourcesFallBackToTriggerWhenUnknown() { + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, WiredConditionInputGuard.normalizeUserSource(-100)); + assertEquals(WiredSourceUtil.SOURCE_SELECTOR, WiredConditionInputGuard.normalizeUserSource(WiredSourceUtil.SOURCE_SELECTOR)); + assertEquals(WiredSourceUtil.SOURCE_SIGNAL, WiredConditionInputGuard.normalizeUserSource(WiredSourceUtil.SOURCE_SIGNAL)); + } + + @Test + void userCountRangesArePositiveOrderedAndCapped() { + assertArrayEquals(new int[]{0, 50}, WiredConditionInputGuard.normalizeUserCountRange(-50, 50)); + assertArrayEquals(new int[]{10, 20}, WiredConditionInputGuard.normalizeUserCountRange(20, 10)); + assertArrayEquals(new int[]{1000, 1000}, WiredConditionInputGuard.normalizeUserCountRange(Integer.MAX_VALUE, Integer.MAX_VALUE)); + } + + @Test + void elapsedTimerCyclesArePositiveAndBounded() { + assertEquals(0, WiredConditionInputGuard.normalizeTimerCycles(-1)); + assertEquals(42, WiredConditionInputGuard.normalizeTimerCycles(42)); + assertEquals(WiredConditionInputGuard.MAX_TIMER_CYCLES, + WiredConditionInputGuard.normalizeTimerCycles(Integer.MAX_VALUE)); + } +} From 4479763f1263b681707108f01b15f0240e331846 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 18:32:24 +0200 Subject: [PATCH 27/43] fix(wired): bound furni condition inputs --- .../WiredConditionFurniHaveFurni.java | 20 ++--- .../WiredConditionFurniHaveHabbo.java | 23 ++---- .../WiredConditionNotFurniHaveFurni.java | 20 ++--- .../WiredConditionNotFurniHaveHabbo.java | 22 ++---- .../WiredConditionTriggerOnFurni.java | 24 +++--- .../WiredFurniConditionInputGuard.java | 76 +++++++++++++++++++ .../WiredFurniConditionInputGuardTest.java | 41 ++++++++++ 7 files changed, 156 insertions(+), 70 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredFurniConditionInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredFurniConditionInputGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveFurni.java index 01eae2ad..d83d291b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveFurni.java @@ -97,9 +97,9 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.all = data.all; - this.furniSource = data.furniSource; + this.furniSource = WiredFurniConditionInputGuard.normalizeFurniSource(data.furniSource); - for(int id : data.itemIds) { + for(int id : WiredFurniConditionInputGuard.sanitizeItemIds(data.itemIds, WiredManager.MAXIMUM_FURNI_SELECTION)) { HabboItem item = room.getHabboItem(id); if (item != null) { @@ -114,10 +114,8 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition { this.all = (data[0].equals("1")); if (data.length == 2) { - String[] items = data[1].split(";"); - - for (String s : items) { - HabboItem item = room.getHabboItem(Integer.parseInt(s)); + for (int id : WiredFurniConditionInputGuard.parseLegacyItemIds(data[1], WiredManager.MAXIMUM_FURNI_SELECTION)) { + HabboItem item = room.getHabboItem(id); if (item != null) this.items.add(item); @@ -126,9 +124,7 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition { } this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; } - if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { - this.furniSource = WiredSourceUtil.SOURCE_SELECTED; - } + this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, !this.items.isEmpty()); } @Override @@ -172,14 +168,12 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition { int[] params = settings.getIntParams(); this.all = params[0] == 1; - this.furniSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = (params.length > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER; int count = settings.getFurniIds().length; if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false; - if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { - this.furniSource = WiredSourceUtil.SOURCE_SELECTED; - } + this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, count > 0); this.items.clear(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveHabbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveHabbo.java index 4fb60898..aafd8561 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveHabbo.java @@ -94,10 +94,10 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.furniSource = data.furniSource; + this.furniSource = WiredFurniConditionInputGuard.normalizeFurniSource(data.furniSource); this.all = data.all; - for(int id : data.itemIds) { + for(int id : WiredFurniConditionInputGuard.sanitizeItemIds(data.itemIds, WiredManager.MAXIMUM_FURNI_SELECTION)) { HabboItem item = room.getHabboItem(id); if (item != null) { @@ -107,12 +107,9 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { } else { String[] data = wiredData.split(":"); - if (data.length >= 1) { - - String[] items = data[1].split(";"); - - for (String s : items) { - HabboItem item = room.getHabboItem(Integer.parseInt(s)); + if (data.length >= 2) { + for (int id : WiredFurniConditionInputGuard.parseLegacyItemIds(data[1], WiredManager.MAXIMUM_FURNI_SELECTION)) { + HabboItem item = room.getHabboItem(id); if (item != null) this.items.add(item); @@ -121,9 +118,7 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; this.all = false; } - if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { - this.furniSource = WiredSourceUtil.SOURCE_SELECTED; - } + this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, !this.items.isEmpty()); } @Override @@ -162,11 +157,9 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { int[] params = settings.getIntParams(); this.all = (params.length > 0) && (params[0] == 1); - this.furniSource = (params.length > 1) ? params[1] : ((params.length > 0 && params[0] > 1) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER); + this.furniSource = (params.length > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[1]) : ((params.length > 0 && params[0] > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER); - if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { - this.furniSource = WiredSourceUtil.SOURCE_SELECTED; - } + this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, count > 0); this.items.clear(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniHaveFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniHaveFurni.java index 41f72077..f76be4bf 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniHaveFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniHaveFurni.java @@ -99,9 +99,9 @@ public class WiredConditionNotFurniHaveFurni extends InteractionWiredCondition { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); this.all = data.all; - this.furniSource = data.furniSource; + this.furniSource = WiredFurniConditionInputGuard.normalizeFurniSource(data.furniSource); - for (int id : data.itemIds) { + for (int id : WiredFurniConditionInputGuard.sanitizeItemIds(data.itemIds, WiredManager.MAXIMUM_FURNI_SELECTION)) { HabboItem item = room.getHabboItem(id); if (item != null) { @@ -115,10 +115,8 @@ public class WiredConditionNotFurniHaveFurni extends InteractionWiredCondition { this.all = (data[0].equals("1")); if (data.length == 2) { - String[] items = data[1].split(";"); - - for (String s : items) { - HabboItem item = room.getHabboItem(Integer.parseInt(s)); + for (int id : WiredFurniConditionInputGuard.parseLegacyItemIds(data[1], WiredManager.MAXIMUM_FURNI_SELECTION)) { + HabboItem item = room.getHabboItem(id); if (item != null) this.items.add(item); @@ -127,9 +125,7 @@ public class WiredConditionNotFurniHaveFurni extends InteractionWiredCondition { } this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; } - if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { - this.furniSource = WiredSourceUtil.SOURCE_SELECTED; - } + this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, !this.items.isEmpty()); } @Override @@ -172,14 +168,12 @@ public class WiredConditionNotFurniHaveFurni extends InteractionWiredCondition { if(settings.getIntParams().length < 1) return false; int[] params = settings.getIntParams(); this.all = params[0] == 1; - this.furniSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = (params.length > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER; int count = settings.getFurniIds().length; if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false; - if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { - this.furniSource = WiredSourceUtil.SOURCE_SELECTED; - } + this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, count > 0); this.items.clear(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniHaveHabbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniHaveHabbo.java index a7d85525..7699bbdd 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniHaveHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniHaveHabbo.java @@ -95,10 +95,10 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition { if (wiredData.startsWith("{")) { WiredConditionFurniHaveHabbo.JsonData data = WiredManager.getGson().fromJson(wiredData, WiredConditionFurniHaveHabbo.JsonData.class); - this.furniSource = data.furniSource; + this.furniSource = WiredFurniConditionInputGuard.normalizeFurniSource(data.furniSource); this.all = data.all; - for(int id : data.itemIds) { + for(int id : WiredFurniConditionInputGuard.sanitizeItemIds(data.itemIds, WiredManager.MAXIMUM_FURNI_SELECTION)) { HabboItem item = room.getHabboItem(id); if (item != null) { @@ -108,11 +108,9 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition { } else { String[] data = wiredData.split(":"); - if (data.length >= 1) { - String[] items = data[1].split(";"); - - for (String s : items) { - HabboItem item = room.getHabboItem(Integer.parseInt(s)); + if (data.length >= 2) { + for (int id : WiredFurniConditionInputGuard.parseLegacyItemIds(data[1], WiredManager.MAXIMUM_FURNI_SELECTION)) { + HabboItem item = room.getHabboItem(id); if (item != null) this.items.add(item); @@ -121,9 +119,7 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition { this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; this.all = false; } - if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { - this.furniSource = WiredSourceUtil.SOURCE_SELECTED; - } + this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, !this.items.isEmpty()); } @Override @@ -161,11 +157,9 @@ public class WiredConditionNotFurniHaveHabbo extends InteractionWiredCondition { int[] params = settings.getIntParams(); this.all = (params.length > 0) && (params[0] == 1); - this.furniSource = (params.length > 1) ? params[1] : ((params.length > 0 && params[0] > 1) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER); + this.furniSource = (params.length > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[1]) : ((params.length > 0 && params[0] > 1) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER); - if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { - this.furniSource = WiredSourceUtil.SOURCE_SELECTED; - } + this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, count > 0); this.items.clear(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggerOnFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggerOnFurni.java index 82054911..4c13c669 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggerOnFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggerOnFurni.java @@ -109,11 +109,11 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.furniSource = data.furniSource; - this.userSource = data.userSource; + this.furniSource = WiredFurniConditionInputGuard.normalizeFurniSource(data.furniSource); + this.userSource = WiredFurniConditionInputGuard.normalizeUserSource(data.userSource); this.quantifier = this.normalizeQuantifier(data.quantifier); - for(int id : data.itemIds) { + for(int id : WiredFurniConditionInputGuard.sanitizeItemIds(data.itemIds, WiredManager.MAXIMUM_FURNI_SELECTION)) { HabboItem item = room.getHabboItem(id); if (item != null) { @@ -121,10 +121,8 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { } } } else { - String[] data = wiredData.split(";"); - - for (String s : data) { - HabboItem item = room.getHabboItem(Integer.parseInt(s)); + for (int id : WiredFurniConditionInputGuard.parseLegacyItemIds(wiredData, WiredManager.MAXIMUM_FURNI_SELECTION)) { + HabboItem item = room.getHabboItem(id); if (item != null) { this.items.add(item); @@ -134,9 +132,7 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { this.userSource = WiredSourceUtil.SOURCE_TRIGGER; this.quantifier = QUANTIFIER_ALL; } - if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { - this.furniSource = WiredSourceUtil.SOURCE_SELECTED; - } + this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, !this.items.isEmpty()); } @Override @@ -182,13 +178,11 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false; int[] params = settings.getIntParams(); - this.furniSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; - this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = (params.length > 0) ? WiredFurniConditionInputGuard.normalizeFurniSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER; + this.userSource = (params.length > 1) ? WiredFurniConditionInputGuard.normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER; this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL; - if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { - this.furniSource = WiredSourceUtil.SOURCE_SELECTED; - } + this.furniSource = WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(this.furniSource, count > 0); this.items.clear(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredFurniConditionInputGuard.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredFurniConditionInputGuard.java new file mode 100644 index 00000000..dfec95a9 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredFurniConditionInputGuard.java @@ -0,0 +1,76 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public final class WiredFurniConditionInputGuard { + private WiredFurniConditionInputGuard() { + } + + public static int normalizeFurniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + case WiredSourceUtil.SOURCE_TRIGGER: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + public static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + public static int selectedOrNormalizedFurniSource(int value, boolean hasSelectedItems) { + int source = normalizeFurniSource(value); + return (hasSelectedItems && source == WiredSourceUtil.SOURCE_TRIGGER) + ? WiredSourceUtil.SOURCE_SELECTED + : source; + } + + public static List sanitizeItemIds(Collection itemIds, int maxCount) { + List result = new ArrayList<>(); + if (itemIds == null || maxCount < 1) { + return result; + } + + for (Integer itemId : itemIds) { + if (itemId == null || itemId < 1 || result.size() >= maxCount) { + continue; + } + + result.add(itemId); + } + + return result; + } + + public static List parseLegacyItemIds(String value, int maxCount) { + List result = new ArrayList<>(); + if (value == null || value.isBlank() || maxCount < 1) { + return result; + } + + for (String part : value.split("[;,:\\t]")) { + if (result.size() >= maxCount) { + break; + } + + try { + int id = Integer.parseInt(part.trim()); + if (id > 0) { + result.add(id); + } + } catch (NumberFormatException ignored) { + // Ignore malformed legacy item ids. + } + } + + return result; + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredFurniConditionInputGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredFurniConditionInputGuardTest.java new file mode 100644 index 00000000..3acc3ec6 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredFurniConditionInputGuardTest.java @@ -0,0 +1,41 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; + +class WiredFurniConditionInputGuardTest { + + @Test + void furniSourcesFallBackToTriggerWhenUnknown() { + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, WiredFurniConditionInputGuard.normalizeFurniSource(-1)); + assertEquals(WiredSourceUtil.SOURCE_SELECTOR, WiredFurniConditionInputGuard.normalizeFurniSource(WiredSourceUtil.SOURCE_SELECTOR)); + assertEquals(WiredSourceUtil.SOURCE_SELECTED, WiredFurniConditionInputGuard.normalizeFurniSource(WiredSourceUtil.SOURCE_SELECTED)); + } + + @Test + void selectedItemsPromoteTriggerSourceToSelected() { + assertEquals(WiredSourceUtil.SOURCE_SELECTED, + WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(WiredSourceUtil.SOURCE_TRIGGER, true)); + assertEquals(WiredSourceUtil.SOURCE_SELECTOR, + WiredFurniConditionInputGuard.selectedOrNormalizedFurniSource(WiredSourceUtil.SOURCE_SELECTOR, true)); + } + + @Test + void itemIdsIgnoreInvalidValuesAndRespectCap() { + assertIterableEquals(Arrays.asList(4, 9), + WiredFurniConditionInputGuard.sanitizeItemIds(Arrays.asList(-1, 4, null, 9, 10), 2)); + } + + @Test + void legacyItemIdsIgnoreMalformedParts() { + assertIterableEquals(Arrays.asList(10, 20, 30), + WiredFurniConditionInputGuard.parseLegacyItemIds("10;bad;-1;20\t30", 5)); + assertIterableEquals(Arrays.asList(10, 20), + WiredFurniConditionInputGuard.parseLegacyItemIds("10;20;30", 2)); + } +} From a5dabd924ec28c4975d38de89277ad98592551d6 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 18:37:25 +0200 Subject: [PATCH 28/43] fix(wired): bound match position inputs --- .../WiredConditionMatchStatePosition.java | 46 +++++++--- .../WiredMatchPositionInputGuard.java | 92 +++++++++++++++++++ .../WiredMatchPositionInputGuardTest.java | 33 +++++++ 3 files changed, 158 insertions(+), 13 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredMatchPositionInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredMatchPositionInputGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java index 36e721fc..2a306797 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java @@ -87,7 +87,7 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition this.direction = params[1] == 1; this.position = params[2] == 1; this.altitude = (params.length > 3) && (params[3] == 1); - this.furniSource = (params.length > 4) ? params[4] : ((params.length > 3 && params[3] > 1) ? params[3] : WiredSourceUtil.SOURCE_TRIGGER); + this.furniSource = (params.length > 4) ? WiredMatchPositionInputGuard.normalizeFurniSource(params[4], false) : ((params.length > 3 && params[3] > 1) ? WiredMatchPositionInputGuard.normalizeFurniSource(params[3], false) : WiredSourceUtil.SOURCE_TRIGGER); this.quantifier = (params.length > 5) ? this.normalizeQuantifier(params[5]) : QUANTIFIER_ALL; Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); @@ -108,6 +108,8 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition this.settings.add(new WiredMatchFurniSetting(item.getId(), item.getExtradata(), item.getRotation(), item.getX(), item.getY(), item.getZ())); } + this.furniSource = WiredMatchPositionInputGuard.normalizeFurniSource(this.furniSource, !this.settings.isEmpty()); + return true; } @@ -255,27 +257,23 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition this.position = data.position; this.direction = data.direction; this.altitude = data.altitude; - if (data.settings != null) { - this.settings.addAll(data.settings); - } - this.furniSource = data.furniSource; + this.settings.addAll(WiredMatchPositionInputGuard.sanitizeSettings(data.settings, room)); + this.furniSource = WiredMatchPositionInputGuard.normalizeFurniSource(data.furniSource, !this.settings.isEmpty()); this.quantifier = this.normalizeQuantifier(data.quantifier); } else { String[] data = wiredData.split(":"); if (data.length >= 5) { try { - int itemCount = Integer.parseInt(data[0]); + int itemCount = Math.min(Integer.parseInt(data[0]), WiredManager.MAXIMUM_FURNI_SELECTION); String[] items = data[1].split(";"); for (int i = 0; i < itemCount && i < items.length; i++) { - String[] stuff = items[i].split("-"); - - if (stuff.length >= 6) - this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4]), Double.parseDouble(stuff[5]))); - else if (stuff.length >= 5) - this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4]))); + WiredMatchFurniSetting setting = this.parseLegacySetting(items[i], room); + if (setting != null) { + this.settings.add(setting); + } } this.state = data[2].equals("1"); @@ -287,11 +285,33 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition } this.altitude = false; - this.furniSource = this.settings.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; + this.furniSource = WiredMatchPositionInputGuard.normalizeFurniSource(WiredSourceUtil.SOURCE_TRIGGER, !this.settings.isEmpty()); this.quantifier = QUANTIFIER_ALL; } } + private WiredMatchFurniSetting parseLegacySetting(String value, Room room) { + String[] parts = value.split("-", 6); + if (parts.length < 5) { + return null; + } + + try { + double z = (parts.length >= 6) ? Double.parseDouble(parts[5]) : 0.0D; + return WiredMatchPositionInputGuard.sanitizeParts( + Integer.parseInt(parts[0]), + parts[1], + Integer.parseInt(parts[2]), + Integer.parseInt(parts[3]), + Integer.parseInt(parts[4]), + z, + room + ); + } catch (NumberFormatException ignored) { + return null; + } + } + @Override public void onPickUp() { this.settings.clear(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredMatchPositionInputGuard.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredMatchPositionInputGuard.java new file mode 100644 index 00000000..563387d0 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredMatchPositionInputGuard.java @@ -0,0 +1,92 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.users.HabboItem; +import com.eu.habbo.habbohotel.wired.WiredMatchFurniSetting; +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public final class WiredMatchPositionInputGuard { + public static final int MAX_STATE_LENGTH = 512; + + private WiredMatchPositionInputGuard() { + } + + public static int normalizeFurniSource(int value, boolean hasSelectedSettings) { + int source = switch (value) { + case WiredSourceUtil.SOURCE_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, + WiredSourceUtil.SOURCE_SIGNAL, WiredSourceUtil.SOURCE_TRIGGER -> value; + default -> WiredSourceUtil.SOURCE_TRIGGER; + }; + + return (hasSelectedSettings && source == WiredSourceUtil.SOURCE_TRIGGER) + ? WiredSourceUtil.SOURCE_SELECTED + : source; + } + + public static List sanitizeSettings(Collection settings, Room room) { + List result = new ArrayList<>(); + if (settings == null || room == null) { + return result; + } + + for (WiredMatchFurniSetting setting : settings) { + WiredMatchFurniSetting normalized = sanitizeSetting(setting, room); + if (normalized != null) { + result.add(normalized); + } + + if (result.size() >= WiredManager.MAXIMUM_FURNI_SELECTION) { + break; + } + } + + return result; + } + + public static WiredMatchFurniSetting sanitizeSetting(WiredMatchFurniSetting setting, Room room) { + if (setting == null || room == null) { + return null; + } + + return sanitizeParts(setting.item_id, setting.state, setting.rotation, setting.x, setting.y, setting.z, room); + } + + public static WiredMatchFurniSetting sanitizeParts(int itemId, String state, int rotation, int x, int y, double z, Room room) { + if (itemId < 1 || room == null) { + return null; + } + + HabboItem item = room.getHabboItem(itemId); + if (item == null || rotation < 0 || rotation > 7 || !Double.isFinite(z)) { + return null; + } + + if (x < Short.MIN_VALUE || x > Short.MAX_VALUE || y < Short.MIN_VALUE || y > Short.MAX_VALUE) { + return null; + } + + if (room.getLayout() != null && room.getLayout().getTile((short) x, (short) y) == null) { + return null; + } + + return new WiredMatchFurniSetting(itemId, normalizeState(state), rotation, x, y, z); + } + + public static String normalizeState(String state) { + if (state == null) { + return ""; + } + + String normalized = state.replace('\t', ' ').replace('\r', ' ').replace('\n', ' '); + if (normalized.length() > MAX_STATE_LENGTH) { + return normalized.substring(0, MAX_STATE_LENGTH); + } + + return normalized; + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredMatchPositionInputGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredMatchPositionInputGuardTest.java new file mode 100644 index 00000000..3e5878df --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredMatchPositionInputGuardTest.java @@ -0,0 +1,33 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class WiredMatchPositionInputGuardTest { + + @Test + void furniSourcesFallBackToTriggerWhenUnknown() { + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, WiredMatchPositionInputGuard.normalizeFurniSource(-1, false)); + assertEquals(WiredSourceUtil.SOURCE_SELECTOR, WiredMatchPositionInputGuard.normalizeFurniSource(WiredSourceUtil.SOURCE_SELECTOR, false)); + assertEquals(WiredSourceUtil.SOURCE_SIGNAL, WiredMatchPositionInputGuard.normalizeFurniSource(WiredSourceUtil.SOURCE_SIGNAL, false)); + } + + @Test + void selectedSettingsPromoteTriggerSourceToSelected() { + assertEquals(WiredSourceUtil.SOURCE_SELECTED, + WiredMatchPositionInputGuard.normalizeFurniSource(WiredSourceUtil.SOURCE_TRIGGER, true)); + } + + @Test + void stateIsNullSafeSingleLineAndBounded() { + assertEquals("", WiredMatchPositionInputGuard.normalizeState(null)); + assertEquals("a b c", WiredMatchPositionInputGuard.normalizeState("a\tb\nc")); + String longState = "x".repeat(WiredMatchPositionInputGuard.MAX_STATE_LENGTH + 10); + String normalized = WiredMatchPositionInputGuard.normalizeState(longState); + assertEquals(WiredMatchPositionInputGuard.MAX_STATE_LENGTH, normalized.length()); + assertFalse(normalized.contains("\n")); + } +} From 5dfa8df5f41ea8acf299ca47da541fc60b5699cb Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 18:40:48 +0200 Subject: [PATCH 29/43] fix(wired): bound date range inputs --- .../WiredConditionDateRangeActive.java | 24 ++++++++++++----- .../conditions/WiredDateRangeInputGuard.java | 21 +++++++++++++++ .../WiredDateRangeInputGuardTest.java | 26 +++++++++++++++++++ 3 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredDateRangeInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredDateRangeInputGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionDateRangeActive.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionDateRangeActive.java index 0b92d4e6..80c5f75f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionDateRangeActive.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionDateRangeActive.java @@ -53,8 +53,7 @@ public class WiredConditionDateRangeActive extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { if(settings.getIntParams().length < 2) return false; - this.startDate = settings.getIntParams()[0]; - this.endDate = settings.getIntParams()[1]; + this.applyRange(settings.getIntParams()[0], settings.getIntParams()[1]); return true; } @@ -81,19 +80,26 @@ public class WiredConditionDateRangeActive extends InteractionWiredCondition { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + this.applyRange(0, 0); + return; + } if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.startDate = data.startDate; - this.endDate = data.endDate; + if (data == null) { + this.applyRange(0, 0); + return; + } + this.applyRange(data.startDate, data.endDate); } else { String[] data = wiredData.split("\t"); if (data.length == 2) { try { - this.startDate = Integer.parseInt(data[0]); - this.endDate = Integer.parseInt(data[1]); + this.applyRange(Integer.parseInt(data[0]), Integer.parseInt(data[1])); } catch (Exception e) { + this.applyRange(0, 0); } } } @@ -105,6 +111,12 @@ public class WiredConditionDateRangeActive extends InteractionWiredCondition { this.endDate = 0; } + private void applyRange(int startDate, int endDate) { + int[] range = WiredDateRangeInputGuard.normalizeRange(startDate, endDate); + this.startDate = range[0]; + this.endDate = range[1]; + } + static class JsonData { int startDate; int endDate; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredDateRangeInputGuard.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredDateRangeInputGuard.java new file mode 100644 index 00000000..3661bd57 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredDateRangeInputGuard.java @@ -0,0 +1,21 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +public final class WiredDateRangeInputGuard { + private WiredDateRangeInputGuard() { + } + + public static int[] normalizeRange(int startDate, int endDate) { + int start = normalizeTimestamp(startDate); + int end = normalizeTimestamp(endDate); + + if (start > end) { + return new int[]{0, 0}; + } + + return new int[]{start, end}; + } + + public static int normalizeTimestamp(int value) { + return Math.max(0, value); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredDateRangeInputGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredDateRangeInputGuardTest.java new file mode 100644 index 00000000..c31bb5d3 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredDateRangeInputGuardTest.java @@ -0,0 +1,26 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WiredDateRangeInputGuardTest { + + @Test + void timestampsAreNonNegative() { + assertEquals(0, WiredDateRangeInputGuard.normalizeTimestamp(-1)); + assertEquals(42, WiredDateRangeInputGuard.normalizeTimestamp(42)); + } + + @Test + void validRangesArePreserved() { + assertArrayEquals(new int[]{100, 200}, WiredDateRangeInputGuard.normalizeRange(100, 200)); + } + + @Test + void negativeAndInvertedRangesBecomeInactive() { + assertArrayEquals(new int[]{0, 0}, WiredDateRangeInputGuard.normalizeRange(-10, -1)); + assertArrayEquals(new int[]{0, 0}, WiredDateRangeInputGuard.normalizeRange(200, 100)); + } +} From cf307e44dab7f48bb7de1e64e475d127ef676d0e Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 18:50:06 +0200 Subject: [PATCH 30/43] fix(wired): bound user action inputs --- .../WiredConditionUserPerformsAction.java | 10 +++++++-- .../conditions/WiredUserActionInputGuard.java | 14 ++++++++++++ .../WiredUserActionInputGuardTest.java | 22 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredUserActionInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredUserActionInputGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionUserPerformsAction.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionUserPerformsAction.java index 5e49a6a1..1ce928cc 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionUserPerformsAction.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionUserPerformsAction.java @@ -87,7 +87,13 @@ public class WiredConditionUserPerformsAction extends InteractionWiredCondition return; } - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException ignored) { + this.resetSettings(); + return; + } if (data == null) { return; @@ -253,7 +259,7 @@ public class WiredConditionUserPerformsAction extends InteractionWiredCondition } long timestamp = (Long) timestampValue; - if ((System.currentTimeMillis() - timestamp) > TRANSIENT_ACTION_WINDOW_MS) { + if (!WiredUserActionInputGuard.isRecentTimestamp(timestamp, System.currentTimeMillis(), TRANSIENT_ACTION_WINDOW_MS)) { return false; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredUserActionInputGuard.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredUserActionInputGuard.java new file mode 100644 index 00000000..a0e5771b --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredUserActionInputGuard.java @@ -0,0 +1,14 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +public final class WiredUserActionInputGuard { + private WiredUserActionInputGuard() { + } + + public static boolean isRecentTimestamp(long timestamp, long now, long windowMs) { + if (timestamp < 1 || timestamp > now || windowMs < 1) { + return false; + } + + return (now - timestamp) <= windowMs; + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredUserActionInputGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredUserActionInputGuardTest.java new file mode 100644 index 00000000..b155c8bf --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredUserActionInputGuardTest.java @@ -0,0 +1,22 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class WiredUserActionInputGuardTest { + + @Test + void rejectsInvalidOrFutureTimestamps() { + assertFalse(WiredUserActionInputGuard.isRecentTimestamp(0, 1000, 5000)); + assertFalse(WiredUserActionInputGuard.isRecentTimestamp(1500, 1000, 5000)); + assertFalse(WiredUserActionInputGuard.isRecentTimestamp(900, 1000, 0)); + } + + @Test + void acceptsTimestampsInsideWindowOnly() { + assertTrue(WiredUserActionInputGuard.isRecentTimestamp(900, 1000, 5000)); + assertFalse(WiredUserActionInputGuard.isRecentTimestamp(100, 1000, 500)); + } +} From 3304edafb7105161aab3cf81d456b211d5125c18 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 18:54:27 +0200 Subject: [PATCH 31/43] fix(wired): bound user condition inputs --- .../WiredConditionHabboHasEffect.java | 30 ++++++++--- .../WiredConditionHabboHasHandItem.java | 51 +++++++++++-------- .../WiredConditionHabboWearsBadge.java | 26 +++++++--- .../WiredUserConditionInputGuard.java | 45 ++++++++++++++++ .../WiredUserConditionInputGuardTest.java | 33 ++++++++++++ 5 files changed, 151 insertions(+), 34 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredUserConditionInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredUserConditionInputGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasEffect.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasEffect.java index 2075513d..e49ceddf 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasEffect.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasEffect.java @@ -87,14 +87,32 @@ public class WiredConditionHabboHasEffect extends InteractionWiredCondition { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + this.onPickUp(); + return; + } if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.effectId = data.effectId; - this.userSource = data.userSource; + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException ignored) { + this.onPickUp(); + return; + } + if (data == null) { + this.onPickUp(); + return; + } + this.effectId = WiredUserConditionInputGuard.normalizeEffectId(data.effectId); + this.userSource = WiredUserConditionInputGuard.normalizeUserSource(data.userSource); this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY); } else { - this.effectId = Integer.parseInt(wiredData); + try { + this.effectId = WiredUserConditionInputGuard.normalizeEffectId(Integer.parseInt(wiredData)); + } catch (NumberFormatException ignored) { + this.effectId = 0; + } this.userSource = WiredSourceUtil.SOURCE_TRIGGER; this.quantifier = QUANTIFIER_ANY; } @@ -134,8 +152,8 @@ public class WiredConditionHabboHasEffect extends InteractionWiredCondition { public boolean saveData(WiredSettings settings) { if(settings.getIntParams().length < 1) return false; int[] params = settings.getIntParams(); - this.effectId = params[0]; - this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + this.effectId = WiredUserConditionInputGuard.normalizeEffectId(params[0]); + this.userSource = (params.length > 1) ? WiredUserConditionInputGuard.normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER; this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2], QUANTIFIER_ANY) : QUANTIFIER_ANY; return true; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasHandItem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasHandItem.java index d949e4bd..c535e271 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasHandItem.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasHandItem.java @@ -10,15 +10,12 @@ import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.messages.ServerMessage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; public class WiredConditionHabboHasHandItem extends InteractionWiredCondition { - private static final Logger LOGGER = LoggerFactory.getLogger(WiredConditionHabboHasHandItem.class); protected static final int QUANTIFIER_ALL = 0; protected static final int QUANTIFIER_ANY = 1; @@ -62,9 +59,9 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { if(settings.getIntParams().length < 1) return false; - this.handItem = this.normalizeHandItem(settings.getIntParams()[0]); + this.handItem = WiredUserConditionInputGuard.normalizeHandItemId(settings.getIntParams()[0]); int[] params = settings.getIntParams(); - this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + this.userSource = (params.length > 1) ? WiredUserConditionInputGuard.normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER; this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL; return true; @@ -99,21 +96,35 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { - try { - String wiredData = set.getString("wired_data"); + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + this.onPickUp(); + return; + } - if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.handItem = this.normalizeHandItem(data.handItemId); - this.userSource = data.userSource; - this.quantifier = this.normalizeQuantifier(data.quantifier); - } else { - this.handItem = this.normalizeHandItem(Integer.parseInt(wiredData)); - this.userSource = WiredSourceUtil.SOURCE_TRIGGER; - this.quantifier = QUANTIFIER_ALL; + if (wiredData.startsWith("{")) { + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException ignored) { + this.onPickUp(); + return; } - } catch (Exception e) { - LOGGER.error("Caught exception", e); + if (data == null) { + this.onPickUp(); + return; + } + this.handItem = WiredUserConditionInputGuard.normalizeHandItemId(data.handItemId); + this.userSource = WiredUserConditionInputGuard.normalizeUserSource(data.userSource); + this.quantifier = this.normalizeQuantifier(data.quantifier); + } else { + try { + this.handItem = WiredUserConditionInputGuard.normalizeHandItemId(Integer.parseInt(wiredData)); + } catch (NumberFormatException ignored) { + this.handItem = 0; + } + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; } } @@ -156,10 +167,6 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition { return true; } - protected int normalizeHandItem(int value) { - return Math.max(0, value); - } - protected int normalizeQuantifier(int value) { return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboWearsBadge.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboWearsBadge.java index 83c85a8f..6d01aeaf 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboWearsBadge.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboWearsBadge.java @@ -103,14 +103,28 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); + if (wiredData == null) { + this.onPickUp(); + return; + } if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.badge = data.badge; - this.userSource = data.userSource; + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException ignored) { + this.onPickUp(); + return; + } + if (data == null) { + this.onPickUp(); + return; + } + this.badge = WiredUserConditionInputGuard.normalizeBadgeCode(data.badge); + this.userSource = WiredUserConditionInputGuard.normalizeUserSource(data.userSource); this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY); } else { - this.badge = wiredData; + this.badge = WiredUserConditionInputGuard.normalizeBadgeCode(wiredData); this.userSource = WiredSourceUtil.SOURCE_TRIGGER; this.quantifier = QUANTIFIER_ANY; } @@ -147,9 +161,9 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { - this.badge = settings.getStringParam(); + this.badge = WiredUserConditionInputGuard.normalizeBadgeCode(settings.getStringParam()); int[] params = settings.getIntParams(); - this.userSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; + this.userSource = (params.length > 0) ? WiredUserConditionInputGuard.normalizeUserSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER; this.quantifier = (params.length > 1) ? this.normalizeQuantifier(params[1], QUANTIFIER_ANY) : QUANTIFIER_ANY; return true; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredUserConditionInputGuard.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredUserConditionInputGuard.java new file mode 100644 index 00000000..86646983 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredUserConditionInputGuard.java @@ -0,0 +1,45 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; + +public final class WiredUserConditionInputGuard { + public static final int MAX_BADGE_CODE_LENGTH = 64; + public static final int MAX_EFFECT_ID = 10_000; + public static final int MAX_HAND_ITEM_ID = 10_000; + + private WiredUserConditionInputGuard() { + } + + public static String normalizeBadgeCode(String value) { + if (value == null) { + return ""; + } + + String normalized = value.trim().replace('\t', ' ').replace('\r', ' ').replace('\n', ' '); + if (normalized.length() > MAX_BADGE_CODE_LENGTH) { + return normalized.substring(0, MAX_BADGE_CODE_LENGTH); + } + + return normalized; + } + + public static int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + public static int normalizeEffectId(int value) { + if (value < 0) { + return 0; + } + + return Math.min(value, MAX_EFFECT_ID); + } + + public static int normalizeHandItemId(int value) { + if (value < 0) { + return 0; + } + + return Math.min(value, MAX_HAND_ITEM_ID); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredUserConditionInputGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredUserConditionInputGuardTest.java new file mode 100644 index 00000000..a1251715 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredUserConditionInputGuardTest.java @@ -0,0 +1,33 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class WiredUserConditionInputGuardTest { + + @Test + void badgeCodesAreBoundedAndSingleLine() { + assertEquals("", WiredUserConditionInputGuard.normalizeBadgeCode(null)); + assertEquals("ACH Test", WiredUserConditionInputGuard.normalizeBadgeCode(" ACH\tTest\n")); + String normalized = WiredUserConditionInputGuard.normalizeBadgeCode("x".repeat(WiredUserConditionInputGuard.MAX_BADGE_CODE_LENGTH + 10)); + assertEquals(WiredUserConditionInputGuard.MAX_BADGE_CODE_LENGTH, normalized.length()); + assertFalse(normalized.contains("\n")); + } + + @Test + void numericIdsAreNonNegativeAndCapped() { + assertEquals(0, WiredUserConditionInputGuard.normalizeEffectId(-1)); + assertEquals(WiredUserConditionInputGuard.MAX_EFFECT_ID, WiredUserConditionInputGuard.normalizeEffectId(Integer.MAX_VALUE)); + assertEquals(0, WiredUserConditionInputGuard.normalizeHandItemId(-1)); + assertEquals(WiredUserConditionInputGuard.MAX_HAND_ITEM_ID, WiredUserConditionInputGuard.normalizeHandItemId(Integer.MAX_VALUE)); + } + + @Test + void unknownUserSourcesFallBackToTrigger() { + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, WiredUserConditionInputGuard.normalizeUserSource(-1)); + assertEquals(WiredSourceUtil.SOURCE_SELECTOR, WiredUserConditionInputGuard.normalizeUserSource(WiredSourceUtil.SOURCE_SELECTOR)); + } +} From 237c3a3425cb8ea6a9e577981e94793855b011cc Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 19:04:56 +0200 Subject: [PATCH 32/43] fix(wired): bound team condition inputs --- .../WiredConditionTeamGameBase.java | 7 +- .../conditions/WiredConditionTeamHasRank.java | 8 +- .../WiredConditionTeamHasScore.java | 8 +- .../WiredConditionTeamGameBaseTest.java | 91 +++++++++++++++++++ 4 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamGameBaseTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamGameBase.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamGameBase.java index 1b5dd1d9..7fb26faa 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamGameBase.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamGameBase.java @@ -29,6 +29,7 @@ abstract class WiredConditionTeamGameBase extends InteractionWiredCondition { protected static final int COMPARISON_EQUAL = 1; protected static final int COMPARISON_HIGHER = 2; protected static final int TEAM_TRIGGERER = 0; + protected static final int MAX_SCORE = 1_000_000; private static final GameTeamColors[] SUPPORTED_TEAM_COLORS = new GameTeamColors[] { GameTeamColors.RED, @@ -96,7 +97,11 @@ abstract class WiredConditionTeamGameBase extends InteractionWiredCondition { } protected int normalizeScore(int value) { - return Math.max(0, value); + if (value < 0) { + return 0; + } + + return Math.min(value, MAX_SCORE); } protected int normalizeExplicitTeamType(int value) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamHasRank.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamHasRank.java index 58272197..2a634424 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamHasRank.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamHasRank.java @@ -65,7 +65,13 @@ public class WiredConditionTeamHasRank extends WiredConditionTeamGameBase { return; } - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException ignored) { + this.resetSettings(); + return; + } if (data == null) { return; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamHasScore.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamHasScore.java index e85741a6..e5912127 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamHasScore.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamHasScore.java @@ -66,7 +66,13 @@ public class WiredConditionTeamHasScore extends WiredConditionTeamGameBase { return; } - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException ignored) { + this.resetSettings(); + return; + } if (data == null) { return; } diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamGameBaseTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamGameBaseTest.java new file mode 100644 index 00000000..a4d94eae --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTeamGameBaseTest.java @@ -0,0 +1,91 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WiredConditionTeamGameBaseTest { + + private final ExposedTeamGameBase guard = new ExposedTeamGameBase(); + + @Test + void scoresAreNonNegativeAndCapped() { + assertEquals(0, this.guard.score(-1)); + assertEquals(42, this.guard.score(42)); + assertEquals(WiredConditionTeamGameBase.MAX_SCORE, this.guard.score(Integer.MAX_VALUE)); + } + + @Test + void userSourcesFallBackToTriggerWhenUnknown() { + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, this.guard.userSource(-1)); + assertEquals(WiredSourceUtil.SOURCE_SELECTOR, this.guard.userSource(WiredSourceUtil.SOURCE_SELECTOR)); + } + + @Test + void teamTypesAndPlacementsStayInSupportedRange() { + assertEquals(1, this.guard.placement(-1)); + assertEquals(4, this.guard.placement(4)); + assertEquals(1, this.guard.explicitTeamType(-1)); + assertEquals(4, this.guard.explicitTeamType(4)); + } + + private static class ExposedTeamGameBase extends WiredConditionTeamGameBase { + private ExposedTeamGameBase() { + super(0, 0, null, "", 0, 0); + } + + @Override + public com.eu.habbo.habbohotel.wired.WiredConditionType getType() { + return com.eu.habbo.habbohotel.wired.WiredConditionType.TEAM_HAS_SCORE; + } + + @Override + public boolean evaluate(com.eu.habbo.habbohotel.wired.core.WiredContext ctx) { + return false; + } + + @Override + public boolean execute(com.eu.habbo.habbohotel.rooms.RoomUnit roomUnit, com.eu.habbo.habbohotel.rooms.Room room, Object[] stuff) { + return false; + } + + @Override + public boolean saveData(com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings settings) { + return false; + } + + @Override + public void onPickUp() { + } + + @Override + public String getWiredData() { + return ""; + } + + @Override + public void loadWiredData(java.sql.ResultSet set, com.eu.habbo.habbohotel.rooms.Room room) { + } + + @Override + public void serializeWiredData(com.eu.habbo.messages.ServerMessage message, com.eu.habbo.habbohotel.rooms.Room room) { + } + + int score(int value) { + return this.normalizeScore(value); + } + + int userSource(int value) { + return this.normalizeUserSource(value); + } + + int placement(int value) { + return this.normalizePlacement(value); + } + + int explicitTeamType(int value) { + return this.normalizeExplicitTeamType(value); + } + } +} From a2c958684c2de6e4310cf0ea4998516319cc14ce Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 19:09:52 +0200 Subject: [PATCH 33/43] fix(wired): bound furni condition payloads --- .../WiredConditionCounterTimeMatches.java | 53 ++++++++++++++----- .../conditions/WiredConditionHasAltitude.java | 25 ++++++--- .../WiredConditionFurniPayloadGuardTest.java | 41 ++++++++++++++ 3 files changed, 99 insertions(+), 20 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniPayloadGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionCounterTimeMatches.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionCounterTimeMatches.java index a28ea00b..769fbae7 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionCounterTimeMatches.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionCounterTimeMatches.java @@ -111,19 +111,21 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { - this.items.clear(); - this.comparison = COMPARISON_EQUAL; - this.minutes = 0; - this.halfSecondSteps = 0; - this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; - this.quantifier = QUANTIFIER_ALL; + this.resetSettings(); String wiredData = set.getString("wired_data"); if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) { return; } - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException exception) { + this.resetSettings(); + return; + } + if (data == null) { return; } @@ -131,7 +133,7 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition this.comparison = this.normalizeComparison(data.comparison); this.minutes = this.normalizeMinutes(data.minutes); this.halfSecondSteps = this.normalizeHalfSecondSteps(data.halfSecondSteps); - this.furniSource = data.furniSource; + this.furniSource = this.normalizeFurniSource(data.furniSource); this.quantifier = this.normalizeQuantifier(data.quantifier); if (data.itemIds == null) { @@ -139,6 +141,10 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition } for (Integer id : data.itemIds) { + if (id == null) { + continue; + } + HabboItem item = room.getHabboItem(id); if (item instanceof InteractionGameUpCounter) { this.items.add(item); @@ -195,7 +201,7 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition this.comparison = (params.length > 0) ? this.normalizeComparison(params[0]) : COMPARISON_EQUAL; this.minutes = (params.length > 1) ? this.normalizeMinutes(params[1]) : 0; this.halfSecondSteps = (params.length > 2) ? this.normalizeHalfSecondSteps(params[2]) : 0; - this.furniSource = (params.length > 3) ? params[3] : WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = (params.length > 3) ? this.normalizeFurniSource(params[3]) : WiredSourceUtil.SOURCE_TRIGGER; this.quantifier = (params.length > 4) ? this.normalizeQuantifier(params[4]) : QUANTIFIER_ALL; this.items.clear(); @@ -224,6 +230,15 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition return true; } + private void resetSettings() { + this.items.clear(); + this.comparison = COMPARISON_EQUAL; + this.minutes = 0; + this.halfSecondSteps = 0; + this.furniSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ALL; + } + private void refresh(Room room) { THashSet remove = new THashSet<>(); @@ -256,7 +271,7 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition } } - private int normalizeComparison(int value) { + int normalizeComparison(int value) { if (value < COMPARISON_LESS || value > COMPARISON_GREATER) { return COMPARISON_EQUAL; } @@ -264,18 +279,30 @@ public class WiredConditionCounterTimeMatches extends InteractionWiredCondition return value; } - private int normalizeMinutes(int value) { + int normalizeMinutes(int value) { return Math.max(0, Math.min(MAX_MINUTES, value)); } - private int normalizeHalfSecondSteps(int value) { + int normalizeHalfSecondSteps(int value) { return Math.max(0, Math.min(MAX_HALF_SECOND_STEPS, value)); } - private int normalizeQuantifier(int value) { + int normalizeQuantifier(int value) { return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; } + int normalizeFurniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + case WiredSourceUtil.SOURCE_TRIGGER: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + static class JsonData { int comparison; int minutes; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHasAltitude.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHasAltitude.java index 43a71b8f..0a5f4a64 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHasAltitude.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHasAltitude.java @@ -97,7 +97,14 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition { return; } - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException exception) { + this.onPickUp(); + return; + } + if (data == null) { return; } @@ -112,6 +119,10 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition { } for (Integer id : data.itemIds) { + if (id == null) { + continue; + } + HabboItem item = room.getHabboItem(id); if (item != null) { this.items.add(item); @@ -225,7 +236,7 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition { } } - private int normalizeComparison(int value) { + int normalizeComparison(int value) { if (value < COMPARISON_LESS || value > COMPARISON_GREATER) { return COMPARISON_EQUAL; } @@ -233,11 +244,11 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition { return value; } - private int normalizeQuantifier(int value) { + int normalizeQuantifier(int value) { return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; } - private int normalizeFurniSource(int value) { + int normalizeFurniSource(int value) { switch (value) { case WiredSourceUtil.SOURCE_SELECTED: case WiredSourceUtil.SOURCE_SELECTOR: @@ -249,12 +260,12 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition { } } - private double normalizeAltitude(double value) { + double normalizeAltitude(double value) { double clampedValue = Math.max(0.0D, Math.min(Room.MAXIMUM_FURNI_HEIGHT, value)); return BigDecimal.valueOf(clampedValue).setScale(2, RoundingMode.HALF_UP).doubleValue(); } - private double parseAltitudeOrDefault(String value) { + double parseAltitudeOrDefault(String value) { if (value == null || value.trim().isEmpty()) { return 0.0D; } @@ -266,7 +277,7 @@ public class WiredConditionHasAltitude extends InteractionWiredCondition { } } - private String formatAltitude(double value) { + String formatAltitude(double value) { BigDecimal decimal = BigDecimal.valueOf(this.normalizeAltitude(value)).stripTrailingZeros(); return (decimal.scale() < 0 ? decimal.setScale(0, RoundingMode.DOWN) : decimal).toPlainString(); } diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniPayloadGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniPayloadGuardTest.java new file mode 100644 index 00000000..52bd2714 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniPayloadGuardTest.java @@ -0,0 +1,41 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WiredConditionFurniPayloadGuardTest { + @Test + void counterTimeNormalizesTimeComparisonQuantifierAndSources() { + WiredConditionCounterTimeMatches condition = new WiredConditionCounterTimeMatches(1, 1, null, "", 0, 0); + + assertEquals(1, condition.normalizeComparison(1)); + assertEquals(1, condition.normalizeComparison(-1)); + assertEquals(1, condition.normalizeComparison(99)); + assertEquals(0, condition.normalizeMinutes(-10)); + assertEquals(99, condition.normalizeMinutes(250)); + assertEquals(0, condition.normalizeHalfSecondSteps(-1)); + assertEquals(119, condition.normalizeHalfSecondSteps(500)); + assertEquals(1, condition.normalizeQuantifier(1)); + assertEquals(0, condition.normalizeQuantifier(7)); + assertEquals(WiredSourceUtil.SOURCE_SELECTED, condition.normalizeFurniSource(WiredSourceUtil.SOURCE_SELECTED)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeFurniSource(42_424)); + } + + @Test + void altitudeNormalizesPayloadInputs() { + WiredConditionHasAltitude condition = new WiredConditionHasAltitude(1, 1, null, "", 0, 0); + + assertEquals(1, condition.normalizeComparison(1)); + assertEquals(1, condition.normalizeComparison(-5)); + assertEquals(1, condition.normalizeComparison(9)); + assertEquals(1, condition.normalizeQuantifier(1)); + assertEquals(0, condition.normalizeQuantifier(8)); + assertEquals(WiredSourceUtil.SOURCE_SIGNAL, condition.normalizeFurniSource(WiredSourceUtil.SOURCE_SIGNAL)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeFurniSource(-900)); + assertEquals(0.0D, condition.parseAltitudeOrDefault("nope")); + assertEquals(12.35D, condition.parseAltitudeOrDefault("12.345")); + assertEquals("12.35", condition.formatAltitude(12.345D)); + } +} From dc6a912632e3da74a71574ea18f7c4f913a42cf6 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 19:13:00 +0200 Subject: [PATCH 34/43] fix(wired): bound user condition payloads --- .../conditions/WiredConditionActorDir.java | 14 +++++-- .../WiredConditionTriggererMatch.java | 29 ++++++++++---- .../WiredConditionUserPayloadGuardTest.java | 38 +++++++++++++++++++ 3 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionUserPayloadGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionActorDir.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionActorDir.java index a9670b2e..16eb8f83 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionActorDir.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionActorDir.java @@ -111,7 +111,13 @@ public class WiredConditionActorDir extends InteractionWiredCondition { } if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException exception) { + this.onPickUp(); + return; + } if (data == null) { return; @@ -157,15 +163,15 @@ public class WiredConditionActorDir extends InteractionWiredCondition { return (this.directionMask & (1 << direction)) != 0; } - private int normalizeDirectionMask(int value) { + int normalizeDirectionMask(int value) { return value & ALL_DIRECTIONS_MASK; } - private int normalizeUserSource(int value) { + int normalizeUserSource(int value) { return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; } - private int normalizeQuantifier(int value) { + int normalizeQuantifier(int value) { return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggererMatch.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggererMatch.java index e7f01dcb..8609dda3 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggererMatch.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggererMatch.java @@ -33,6 +33,7 @@ public class WiredConditionTriggererMatch extends InteractionWiredCondition { protected static final int QUANTIFIER_ALL = 0; protected static final int QUANTIFIER_ANY = 1; protected static final int SOURCE_SPECIFIED_USERNAME = 101; + protected static final int MAX_USERNAME_LENGTH = 64; public static final WiredConditionType type = WiredConditionType.TRIGGERER_MATCH; @@ -84,7 +85,14 @@ public class WiredConditionTriggererMatch extends InteractionWiredCondition { return; } - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException exception) { + this.resetSettings(); + return; + } + if (data == null) { return; } @@ -284,7 +292,7 @@ public class WiredConditionTriggererMatch extends InteractionWiredCondition { return ""; } - private int normalizeEntityType(int value) { + int normalizeEntityType(int value) { switch (value) { case ENTITY_HABBO: case ENTITY_PET: @@ -295,19 +303,19 @@ public class WiredConditionTriggererMatch extends InteractionWiredCondition { } } - private int normalizeAvatarMode(int value) { + int normalizeAvatarMode(int value) { return (value == AVATAR_MODE_CERTAIN) ? AVATAR_MODE_CERTAIN : AVATAR_MODE_ANY; } - private int normalizeQuantifier(int value) { + int normalizeQuantifier(int value) { return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; } - private int normalizePrimaryUserSource(int value) { + int normalizePrimaryUserSource(int value) { return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; } - private int normalizeCompareUserSource(int value) { + int normalizeCompareUserSource(int value) { switch (value) { case WiredSourceUtil.SOURCE_CLICKED_USER: case SOURCE_SPECIFIED_USERNAME: @@ -317,8 +325,13 @@ public class WiredConditionTriggererMatch extends InteractionWiredCondition { } } - private String normalizeUsername(String value) { - return (value == null) ? "" : value.trim(); + String normalizeUsername(String value) { + if (value == null) { + return ""; + } + + String normalized = value.trim(); + return normalized.length() <= MAX_USERNAME_LENGTH ? normalized : normalized.substring(0, MAX_USERNAME_LENGTH); } protected static class MatchResult { diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionUserPayloadGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionUserPayloadGuardTest.java new file mode 100644 index 00000000..e2f8e856 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionUserPayloadGuardTest.java @@ -0,0 +1,38 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WiredConditionUserPayloadGuardTest { + @Test + void actorDirectionBoundsMaskSourceAndQuantifier() { + WiredConditionActorDir condition = new WiredConditionActorDir(1, 1, null, "", 0, 0); + + assertEquals(255, condition.normalizeDirectionMask(-1)); + assertEquals(0, condition.normalizeDirectionMask(256)); + assertEquals(WiredSourceUtil.SOURCE_CLICKED_USER, condition.normalizeUserSource(WiredSourceUtil.SOURCE_CLICKED_USER)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeUserSource(123_456)); + assertEquals(1, condition.normalizeQuantifier(1)); + assertEquals(0, condition.normalizeQuantifier(2)); + } + + @Test + void triggererMatchBoundsEntitySourcesQuantifierAndUsername() { + WiredConditionTriggererMatch condition = new WiredConditionTriggererMatch(1, 1, null, "", 0, 0); + + assertEquals(WiredConditionTriggererMatch.ENTITY_HABBO, condition.normalizeEntityType(999)); + assertEquals(WiredConditionTriggererMatch.ENTITY_PET, condition.normalizeEntityType(WiredConditionTriggererMatch.ENTITY_PET)); + assertEquals(WiredConditionTriggererMatch.AVATAR_MODE_CERTAIN, condition.normalizeAvatarMode(1)); + assertEquals(WiredConditionTriggererMatch.AVATAR_MODE_ANY, condition.normalizeAvatarMode(2)); + assertEquals(WiredSourceUtil.SOURCE_SIGNAL, condition.normalizePrimaryUserSource(WiredSourceUtil.SOURCE_SIGNAL)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizePrimaryUserSource(900)); + assertEquals(WiredConditionTriggererMatch.SOURCE_SPECIFIED_USERNAME, condition.normalizeCompareUserSource(WiredConditionTriggererMatch.SOURCE_SPECIFIED_USERNAME)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeCompareUserSource(-1)); + assertEquals(1, condition.normalizeQuantifier(1)); + assertEquals(0, condition.normalizeQuantifier(5)); + assertEquals("tester", condition.normalizeUsername(" tester ")); + assertEquals(WiredConditionTriggererMatch.MAX_USERNAME_LENGTH, condition.normalizeUsername("x".repeat(200)).length()); + } +} From 60eeaca689e1338f73f34c8c6802684dfb7388ef Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 19:18:23 +0200 Subject: [PATCH 35/43] fix(wired): bound count time payloads --- .../conditions/WiredConditionHabboCount.java | 78 ++++++++++++++----- .../WiredConditionLessTimeElapsed.java | 11 ++- .../WiredConditionMoreTimeElapsed.java | 12 ++- .../WiredConditionNotHabboCount.java | 78 ++++++++++++++----- ...redConditionCountTimePayloadGuardTest.java | 50 ++++++++++++ 5 files changed, 185 insertions(+), 44 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionCountTimePayloadGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboCount.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboCount.java index 98010557..ea2d137b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboCount.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboCount.java @@ -16,6 +16,7 @@ import java.sql.SQLException; public class WiredConditionHabboCount extends InteractionWiredCondition { public static final WiredConditionType type = WiredConditionType.USER_COUNT; + static final int MAX_USER_COUNT_LIMIT = 1000; private int lowerLimit = 0; private int upperLimit = 50; @@ -31,6 +32,10 @@ public class WiredConditionHabboCount extends InteractionWiredCondition { @Override public boolean evaluate(WiredContext ctx) { + if (ctx == null || ctx.room() == null) { + return false; + } + int count = (this.userSource == WiredSourceUtil.SOURCE_TRIGGER) ? ctx.room().getUserCount() : WiredSourceUtil.resolveUsers(ctx, this.userSource).size(); @@ -55,26 +60,40 @@ public class WiredConditionHabboCount extends InteractionWiredCondition { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.lowerLimit = data.lowerLimit; - this.upperLimit = data.upperLimit; - this.userSource = data.userSource; - } else { - String[] data = wiredData.split(":"); - - if (data.length >= 2) { - try { - this.lowerLimit = Integer.parseInt(data[0].trim()); - this.upperLimit = Integer.parseInt(data[1].trim()); - } catch (NumberFormatException ignored) { - // malformed legacy data — keep the constructed defaults - } + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException exception) { + this.onPickUp(); + return; } - this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + + if (data == null) { + return; + } + + this.setLimits(data.lowerLimit, data.upperLimit); + this.userSource = this.normalizeUserSource(data.userSource); + return; } + + String[] data = wiredData.split(":"); + if (data.length >= 2) { + try { + this.setLimits(Integer.parseInt(data[0].trim()), Integer.parseInt(data[1].trim())); + } catch (NumberFormatException ignored) { + this.onPickUp(); + } + } + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; } @Override @@ -109,15 +128,36 @@ public class WiredConditionHabboCount extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { - if(settings.getIntParams().length < 2) return false; - this.lowerLimit = settings.getIntParams()[0]; - this.upperLimit = settings.getIntParams()[1]; + if (settings.getIntParams().length < 2) return false; int[] params = settings.getIntParams(); - this.userSource = (params.length > 2) ? params[2] : WiredSourceUtil.SOURCE_TRIGGER; + this.setLimits(params[0], params[1]); + this.userSource = (params.length > 2) ? this.normalizeUserSource(params[2]) : WiredSourceUtil.SOURCE_TRIGGER; return true; } + void setLimits(int lowerLimit, int upperLimit) { + int normalizedLower = this.normalizeLimit(lowerLimit); + int normalizedUpper = this.normalizeLimit(upperLimit); + + if (normalizedLower > normalizedUpper) { + this.lowerLimit = normalizedUpper; + this.upperLimit = normalizedLower; + return; + } + + this.lowerLimit = normalizedLower; + this.upperLimit = normalizedUpper; + } + + int normalizeLimit(int value) { + return Math.max(0, Math.min(MAX_USER_COUNT_LIMIT, value)); + } + + int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + static class JsonData { int lowerLimit; int upperLimit; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionLessTimeElapsed.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionLessTimeElapsed.java index 7086ea7d..d75fb789 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionLessTimeElapsed.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionLessTimeElapsed.java @@ -52,12 +52,13 @@ public class WiredConditionLessTimeElapsed extends InteractionWiredCondition { try { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.cycles = data.cycles; + this.cycles = data == null ? 0 : this.normalizeCycles(data.cycles); } else { if (!wiredData.equals("")) - this.cycles = Integer.parseInt(wiredData); + this.cycles = this.normalizeCycles(Integer.parseInt(wiredData)); } } catch (Exception e) { + this.cycles = 0; } } @@ -90,10 +91,14 @@ public class WiredConditionLessTimeElapsed extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { if(settings.getIntParams().length < 1) return false; - this.cycles = settings.getIntParams()[0]; + this.cycles = this.normalizeCycles(settings.getIntParams()[0]); return true; } + int normalizeCycles(int value) { + return Math.max(0, Math.min(WiredConditionMoreTimeElapsed.MAX_CYCLES, value)); + } + static class JsonData { int cycles; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMoreTimeElapsed.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMoreTimeElapsed.java index 2ae00378..61461bc2 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMoreTimeElapsed.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMoreTimeElapsed.java @@ -16,6 +16,7 @@ import java.sql.SQLException; public class WiredConditionMoreTimeElapsed extends InteractionWiredCondition { private static final WiredConditionType type = WiredConditionType.TIME_MORE_THAN; + static final int MAX_CYCLES = 1_000_000; private int cycles; @@ -52,12 +53,13 @@ public class WiredConditionMoreTimeElapsed extends InteractionWiredCondition { try { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.cycles = data.cycles; + this.cycles = data == null ? 0 : this.normalizeCycles(data.cycles); } else { if (!wiredData.equals("")) - this.cycles = Integer.parseInt(wiredData); + this.cycles = this.normalizeCycles(Integer.parseInt(wiredData)); } } catch (Exception e) { + this.cycles = 0; } } @@ -90,10 +92,14 @@ public class WiredConditionMoreTimeElapsed extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { if(settings.getIntParams().length < 1) return false; - this.cycles = settings.getIntParams()[0]; + this.cycles = this.normalizeCycles(settings.getIntParams()[0]); return true; } + int normalizeCycles(int value) { + return Math.max(0, Math.min(MAX_CYCLES, value)); + } + static class JsonData { int cycles; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboCount.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboCount.java index e993d19f..0780c440 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboCount.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotHabboCount.java @@ -6,8 +6,8 @@ import com.eu.habbo.habbohotel.items.interactions.wired.WiredSettings; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomUnit; import com.eu.habbo.habbohotel.wired.WiredConditionType; -import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; +import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.messages.ServerMessage; @@ -31,6 +31,10 @@ public class WiredConditionNotHabboCount extends InteractionWiredCondition { @Override public boolean evaluate(WiredContext ctx) { + if (ctx == null || ctx.room() == null) { + return false; + } + int count = (this.userSource == WiredSourceUtil.SOURCE_TRIGGER) ? ctx.room().getUserCount() : WiredSourceUtil.resolveUsers(ctx, this.userSource).size(); @@ -55,25 +59,40 @@ public class WiredConditionNotHabboCount extends InteractionWiredCondition { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } if (wiredData.startsWith("{")) { - WiredConditionHabboCount.JsonData data = WiredManager.getGson().fromJson(wiredData, WiredConditionHabboCount.JsonData.class); - this.lowerLimit = data.lowerLimit; - this.upperLimit = data.upperLimit; - this.userSource = data.userSource; - } else { - String[] data = wiredData.split(":"); - if (data.length >= 2) { - try { - this.lowerLimit = Integer.parseInt(data[0].trim()); - this.upperLimit = Integer.parseInt(data[1].trim()); - } catch (NumberFormatException ignored) { - // malformed legacy data — keep the constructed defaults - } + WiredConditionHabboCount.JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, WiredConditionHabboCount.JsonData.class); + } catch (RuntimeException exception) { + this.onPickUp(); + return; } - this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + + if (data == null) { + return; + } + + this.setLimits(data.lowerLimit, data.upperLimit); + this.userSource = this.normalizeUserSource(data.userSource); + return; } + + String[] data = wiredData.split(":"); + if (data.length >= 2) { + try { + this.setLimits(Integer.parseInt(data[0].trim()), Integer.parseInt(data[1].trim())); + } catch (NumberFormatException ignored) { + this.onPickUp(); + } + } + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; } @Override @@ -108,15 +127,36 @@ public class WiredConditionNotHabboCount extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { - if(settings.getIntParams().length < 2) return false; - this.lowerLimit = settings.getIntParams()[0]; - this.upperLimit = settings.getIntParams()[1]; + if (settings.getIntParams().length < 2) return false; int[] params = settings.getIntParams(); - this.userSource = (params.length > 2) ? params[2] : WiredSourceUtil.SOURCE_TRIGGER; + this.setLimits(params[0], params[1]); + this.userSource = (params.length > 2) ? this.normalizeUserSource(params[2]) : WiredSourceUtil.SOURCE_TRIGGER; return true; } + void setLimits(int lowerLimit, int upperLimit) { + int normalizedLower = this.normalizeLimit(lowerLimit); + int normalizedUpper = this.normalizeLimit(upperLimit); + + if (normalizedLower > normalizedUpper) { + this.lowerLimit = normalizedUpper; + this.upperLimit = normalizedLower; + return; + } + + this.lowerLimit = normalizedLower; + this.upperLimit = normalizedUpper; + } + + int normalizeLimit(int value) { + return Math.max(0, Math.min(WiredConditionHabboCount.MAX_USER_COUNT_LIMIT, value)); + } + + int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + static class JsonData { int lowerLimit; int upperLimit; diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionCountTimePayloadGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionCountTimePayloadGuardTest.java new file mode 100644 index 00000000..336bc2a6 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionCountTimePayloadGuardTest.java @@ -0,0 +1,50 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WiredConditionCountTimePayloadGuardTest { + @Test + void userCountLimitsAndSourcesAreBounded() { + WiredConditionHabboCount condition = new WiredConditionHabboCount(1, 1, null, "", 0, 0); + + assertEquals(0, condition.normalizeLimit(-20)); + assertEquals(25, condition.normalizeLimit(25)); + assertEquals(WiredConditionHabboCount.MAX_USER_COUNT_LIMIT, condition.normalizeLimit(50_000)); + assertEquals(WiredSourceUtil.SOURCE_SIGNAL, condition.normalizeUserSource(WiredSourceUtil.SOURCE_SIGNAL)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeUserSource(-55)); + } + + @Test + void invertedUserCountRangesAreSorted() { + WiredConditionHabboCount condition = new WiredConditionHabboCount(1, 1, null, "", 0, 0); + + condition.setLimits(80, 10); + + assertEquals("{\"lowerLimit\":10,\"upperLimit\":80,\"userSource\":0}", condition.getWiredData()); + } + + @Test + void notUserCountUsesSameBounds() { + WiredConditionNotHabboCount condition = new WiredConditionNotHabboCount(1, 1, null, "", 0, 0); + + assertEquals(0, condition.normalizeLimit(-1)); + assertEquals(WiredConditionHabboCount.MAX_USER_COUNT_LIMIT, condition.normalizeLimit(9_999)); + assertEquals(WiredSourceUtil.SOURCE_CLICKED_USER, condition.normalizeUserSource(WiredSourceUtil.SOURCE_CLICKED_USER)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeUserSource(777)); + } + + @Test + void elapsedTimeCyclesAreBounded() { + WiredConditionMoreTimeElapsed more = new WiredConditionMoreTimeElapsed(1, 1, null, "", 0, 0); + WiredConditionLessTimeElapsed less = new WiredConditionLessTimeElapsed(1, 1, null, "", 0, 0); + + assertEquals(0, more.normalizeCycles(-1)); + assertEquals(42, more.normalizeCycles(42)); + assertEquals(WiredConditionMoreTimeElapsed.MAX_CYCLES, more.normalizeCycles(Integer.MAX_VALUE)); + assertEquals(0, less.normalizeCycles(-1)); + assertEquals(WiredConditionMoreTimeElapsed.MAX_CYCLES, less.normalizeCycles(Integer.MAX_VALUE)); + } +} From 25e38fbbeb8f67ce4e641f902cce2faf9af75480 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 19:21:54 +0200 Subject: [PATCH 36/43] fix(wired): bound avatar condition payloads --- .../WiredConditionHabboHasEffect.java | 44 +++++++++++++---- .../WiredConditionHabboHasHandItem.java | 28 +++++++---- .../WiredConditionHabboWearsBadge.java | 45 +++++++++++++++--- .../WiredConditionAvatarPayloadGuardTest.java | 47 +++++++++++++++++++ 4 files changed, 141 insertions(+), 23 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionAvatarPayloadGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasEffect.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasEffect.java index 2075513d..82ca6e6d 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasEffect.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasEffect.java @@ -18,6 +18,7 @@ import java.util.List; public class WiredConditionHabboHasEffect extends InteractionWiredCondition { protected static final int QUANTIFIER_ALL = 0; protected static final int QUANTIFIER_ANY = 1; + protected static final int MAX_EFFECT_ID = 10_000; public static final WiredConditionType type = WiredConditionType.ACTOR_WEARS_EFFECT; @@ -86,17 +87,36 @@ public class WiredConditionHabboHasEffect extends InteractionWiredCondition { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.effectId = data.effectId; - this.userSource = data.userSource; + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException exception) { + this.onPickUp(); + return; + } + + if (data == null) { + return; + } + + this.effectId = this.normalizeEffectId(data.effectId); + this.userSource = this.normalizeUserSource(data.userSource); this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY); } else { - this.effectId = Integer.parseInt(wiredData); - this.userSource = WiredSourceUtil.SOURCE_TRIGGER; - this.quantifier = QUANTIFIER_ANY; + try { + this.effectId = this.normalizeEffectId(Integer.parseInt(wiredData)); + this.userSource = WiredSourceUtil.SOURCE_TRIGGER; + this.quantifier = QUANTIFIER_ANY; + } catch (NumberFormatException exception) { + this.onPickUp(); + } } } @@ -134,8 +154,8 @@ public class WiredConditionHabboHasEffect extends InteractionWiredCondition { public boolean saveData(WiredSettings settings) { if(settings.getIntParams().length < 1) return false; int[] params = settings.getIntParams(); - this.effectId = params[0]; - this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + this.effectId = this.normalizeEffectId(params[0]); + this.userSource = (params.length > 1) ? this.normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER; this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2], QUANTIFIER_ANY) : QUANTIFIER_ANY; return true; @@ -153,6 +173,14 @@ public class WiredConditionHabboHasEffect extends InteractionWiredCondition { return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; } + protected int normalizeEffectId(int value) { + return Math.max(0, Math.min(MAX_EFFECT_ID, value)); + } + + protected int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + static class JsonData { int effectId; int userSource; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasHandItem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasHandItem.java index d949e4bd..da4ccb23 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasHandItem.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboHasHandItem.java @@ -10,17 +10,15 @@ import com.eu.habbo.habbohotel.wired.core.WiredManager; import com.eu.habbo.habbohotel.wired.core.WiredContext; import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; import com.eu.habbo.messages.ServerMessage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; public class WiredConditionHabboHasHandItem extends InteractionWiredCondition { - private static final Logger LOGGER = LoggerFactory.getLogger(WiredConditionHabboHasHandItem.class); protected static final int QUANTIFIER_ALL = 0; protected static final int QUANTIFIER_ANY = 1; + protected static final int MAX_HAND_ITEM_ID = 10_000; public static final WiredConditionType type = WiredConditionType.ACTOR_HAS_HANDITEM; @@ -64,7 +62,7 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition { if(settings.getIntParams().length < 1) return false; this.handItem = this.normalizeHandItem(settings.getIntParams()[0]); int[] params = settings.getIntParams(); - this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + this.userSource = (params.length > 1) ? this.normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER; this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL; return true; @@ -99,13 +97,21 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { - try { - String wiredData = set.getString("wired_data"); + this.onPickUp(); + String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } + try { if (wiredData.startsWith("{")) { JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + if (data == null) { + return; + } + this.handItem = this.normalizeHandItem(data.handItemId); - this.userSource = data.userSource; + this.userSource = this.normalizeUserSource(data.userSource); this.quantifier = this.normalizeQuantifier(data.quantifier); } else { this.handItem = this.normalizeHandItem(Integer.parseInt(wiredData)); @@ -113,7 +119,7 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition { this.quantifier = QUANTIFIER_ALL; } } catch (Exception e) { - LOGGER.error("Caught exception", e); + this.onPickUp(); } } @@ -157,13 +163,17 @@ public class WiredConditionHabboHasHandItem extends InteractionWiredCondition { } protected int normalizeHandItem(int value) { - return Math.max(0, value); + return Math.max(0, Math.min(MAX_HAND_ITEM_ID, value)); } protected int normalizeQuantifier(int value) { return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; } + protected int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + static class JsonData { int handItemId; int userSource; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboWearsBadge.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboWearsBadge.java index 83c85a8f..28c173b8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboWearsBadge.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionHabboWearsBadge.java @@ -20,6 +20,7 @@ import java.util.List; public class WiredConditionHabboWearsBadge extends InteractionWiredCondition { protected static final int QUANTIFIER_ALL = 0; protected static final int QUANTIFIER_ANY = 1; + protected static final int MAX_BADGE_CODE_LENGTH = 64; public static final WiredConditionType type = WiredConditionType.ACTOR_WEARS_BADGE; @@ -37,6 +38,10 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition { @Override public boolean evaluate(WiredContext ctx) { + if (ctx == null || ctx.room() == null) { + return false; + } + Room room = ctx.room(); List targets = WiredSourceUtil.resolveUsers(ctx, this.userSource); if (targets.isEmpty()) return false; @@ -102,15 +107,30 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.badge = data.badge; - this.userSource = data.userSource; + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException exception) { + this.onPickUp(); + return; + } + + if (data == null) { + return; + } + + this.badge = this.normalizeBadge(data.badge); + this.userSource = this.normalizeUserSource(data.userSource); this.quantifier = this.normalizeQuantifier(data.quantifier, QUANTIFIER_ANY); } else { - this.badge = wiredData; + this.badge = this.normalizeBadge(wiredData); this.userSource = WiredSourceUtil.SOURCE_TRIGGER; this.quantifier = QUANTIFIER_ANY; } @@ -147,9 +167,9 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { - this.badge = settings.getStringParam(); + this.badge = this.normalizeBadge(settings.getStringParam()); int[] params = settings.getIntParams(); - this.userSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; + this.userSource = (params.length > 0) ? this.normalizeUserSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER; this.quantifier = (params.length > 1) ? this.normalizeQuantifier(params[1], QUANTIFIER_ANY) : QUANTIFIER_ANY; return true; @@ -167,6 +187,19 @@ public class WiredConditionHabboWearsBadge extends InteractionWiredCondition { return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; } + protected String normalizeBadge(String value) { + if (value == null) { + return ""; + } + + String normalized = value.trim(); + return normalized.length() <= MAX_BADGE_CODE_LENGTH ? normalized : normalized.substring(0, MAX_BADGE_CODE_LENGTH); + } + + protected int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + static class JsonData { String badge; int userSource; diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionAvatarPayloadGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionAvatarPayloadGuardTest.java new file mode 100644 index 00000000..58f0050d --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionAvatarPayloadGuardTest.java @@ -0,0 +1,47 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WiredConditionAvatarPayloadGuardTest { + @Test + void effectIdsSourcesAndQuantifiersAreBounded() { + WiredConditionHabboHasEffect condition = new WiredConditionHabboHasEffect(1, 1, null, "", 0, 0); + + assertEquals(0, condition.normalizeEffectId(-1)); + assertEquals(23, condition.normalizeEffectId(23)); + assertEquals(WiredConditionHabboHasEffect.MAX_EFFECT_ID, condition.normalizeEffectId(Integer.MAX_VALUE)); + assertEquals(WiredSourceUtil.SOURCE_CLICKED_USER, condition.normalizeUserSource(WiredSourceUtil.SOURCE_CLICKED_USER)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeUserSource(777)); + assertEquals(1, condition.normalizeQuantifier(1, 0)); + assertEquals(0, condition.normalizeQuantifier(5, 0)); + } + + @Test + void handItemIdsSourcesAndQuantifiersAreBounded() { + WiredConditionHabboHasHandItem condition = new WiredConditionHabboHasHandItem(1, 1, null, "", 0, 0); + + assertEquals(0, condition.normalizeHandItem(-1)); + assertEquals(9, condition.normalizeHandItem(9)); + assertEquals(WiredConditionHabboHasHandItem.MAX_HAND_ITEM_ID, condition.normalizeHandItem(Integer.MAX_VALUE)); + assertEquals(WiredSourceUtil.SOURCE_SIGNAL, condition.normalizeUserSource(WiredSourceUtil.SOURCE_SIGNAL)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeUserSource(-44)); + assertEquals(1, condition.normalizeQuantifier(1)); + assertEquals(0, condition.normalizeQuantifier(8)); + } + + @Test + void badgeCodesSourcesAndQuantifiersAreBounded() { + WiredConditionHabboWearsBadge condition = new WiredConditionHabboWearsBadge(1, 1, null, "", 0, 0); + + assertEquals("", condition.normalizeBadge(null)); + assertEquals("ADM", condition.normalizeBadge(" ADM ")); + assertEquals(WiredConditionHabboWearsBadge.MAX_BADGE_CODE_LENGTH, condition.normalizeBadge("x".repeat(200)).length()); + assertEquals(WiredSourceUtil.SOURCE_SELECTOR, condition.normalizeUserSource(WiredSourceUtil.SOURCE_SELECTOR)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeUserSource(66)); + assertEquals(1, condition.normalizeQuantifier(1, 0)); + assertEquals(0, condition.normalizeQuantifier(3, 0)); + } +} From 29ed72fca918e9a69bda68c1aacb5e311012e52f Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 19:25:11 +0200 Subject: [PATCH 37/43] fix(wired): bound furni selection payloads --- .../WiredConditionFurniHaveFurni.java | 62 +++++++++++++---- .../WiredConditionFurniHaveHabbo.java | 56 ++++++++++++--- .../WiredConditionTriggerOnFurni.java | 68 +++++++++++++++---- ...nditionFurniSelectionPayloadGuardTest.java | 37 ++++++++++ 4 files changed, 185 insertions(+), 38 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniSelectionPayloadGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveFurni.java index 01eae2ad..1c5ff9e3 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveFurni.java @@ -92,21 +92,40 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.all = data.all; - this.furniSource = data.furniSource; - - for(int id : data.itemIds) { - HabboItem item = room.getHabboItem(id); - - if (item != null) { - this.items.add(item); - } + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException exception) { + this.onPickUp(); + return; } + if (data == null) { + return; + } + + this.all = data.all; + this.furniSource = this.normalizeFurniSource(data.furniSource); + + if (data.itemIds != null && room != null) { + for (Integer id : data.itemIds) { + if (id == null) { + continue; + } + + HabboItem item = room.getHabboItem(id); + if (item != null) { + this.items.add(item); + } + } + } } else { String[] data = wiredData.split(":"); @@ -117,10 +136,13 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition { String[] items = data[1].split(";"); for (String s : items) { - HabboItem item = room.getHabboItem(Integer.parseInt(s)); + try { + HabboItem item = room.getHabboItem(Integer.parseInt(s)); - if (item != null) - this.items.add(item); + if (item != null) + this.items.add(item); + } catch (NumberFormatException ignored) { + } } } } @@ -172,7 +194,7 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition { int[] params = settings.getIntParams(); this.all = params[0] == 1; - this.furniSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = (params.length > 1) ? this.normalizeFurniSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER; int count = settings.getFurniIds().length; if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false; @@ -198,6 +220,18 @@ public class WiredConditionFurniHaveFurni extends InteractionWiredCondition { return true; } + int normalizeFurniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + case WiredSourceUtil.SOURCE_TRIGGER: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + private void refresh() { THashSet items = new THashSet<>(); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveHabbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveHabbo.java index 4fb60898..63d72180 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniHaveHabbo.java @@ -89,19 +89,38 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { - this.items.clear(); + this.onPickUp(); String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.furniSource = data.furniSource; + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException exception) { + this.onPickUp(); + return; + } + + if (data == null) { + return; + } + + this.furniSource = this.normalizeFurniSource(data.furniSource); this.all = data.all; - for(int id : data.itemIds) { - HabboItem item = room.getHabboItem(id); + if (data.itemIds != null && room != null) { + for (Integer id : data.itemIds) { + if (id == null) { + continue; + } - if (item != null) { - this.items.add(item); + HabboItem item = room.getHabboItem(id); + if (item != null) { + this.items.add(item); + } } } } else { @@ -112,10 +131,13 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { String[] items = data[1].split(";"); for (String s : items) { - HabboItem item = room.getHabboItem(Integer.parseInt(s)); + try { + HabboItem item = room.getHabboItem(Integer.parseInt(s)); - if (item != null) - this.items.add(item); + if (item != null) + this.items.add(item); + } catch (NumberFormatException ignored) { + } } } this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; @@ -162,7 +184,7 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { int[] params = settings.getIntParams(); this.all = (params.length > 0) && (params[0] == 1); - this.furniSource = (params.length > 1) ? params[1] : ((params.length > 0 && params[0] > 1) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER); + this.furniSource = (params.length > 1) ? this.normalizeFurniSource(params[1]) : ((params.length > 0 && params[0] > 1) ? this.normalizeFurniSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER); if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { this.furniSource = WiredSourceUtil.SOURCE_SELECTED; @@ -186,6 +208,18 @@ public class WiredConditionFurniHaveHabbo extends InteractionWiredCondition { return true; } + int normalizeFurniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + case WiredSourceUtil.SOURCE_TRIGGER: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + protected boolean hasAvatarOnItem(HabboItem item, Room room, Collection habbos, Collection bots, Collection pets) { RoomTile baseTile = room.getLayout().getTile(item.getX(), item.getY()); if (baseTile == null) return false; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggerOnFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggerOnFurni.java index 82054911..377ec436 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggerOnFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionTriggerOnFurni.java @@ -42,6 +42,10 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { @Override public boolean evaluate(WiredContext ctx) { + if (ctx == null || ctx.room() == null) { + return false; + } + this.refresh(); List userTargets = WiredSourceUtil.resolveUsers(ctx, this.userSource); @@ -104,30 +108,52 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { - this.items.clear(); + this.onPickUp(); String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.furniSource = data.furniSource; - this.userSource = data.userSource; + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException exception) { + this.onPickUp(); + return; + } + + if (data == null) { + return; + } + + this.furniSource = this.normalizeFurniSource(data.furniSource); + this.userSource = this.normalizeUserSource(data.userSource); this.quantifier = this.normalizeQuantifier(data.quantifier); - for(int id : data.itemIds) { - HabboItem item = room.getHabboItem(id); + if (data.itemIds != null && room != null) { + for (Integer id : data.itemIds) { + if (id == null) { + continue; + } - if (item != null) { - this.items.add(item); + HabboItem item = room.getHabboItem(id); + if (item != null) { + this.items.add(item); + } } } } else { String[] data = wiredData.split(";"); for (String s : data) { - HabboItem item = room.getHabboItem(Integer.parseInt(s)); + try { + HabboItem item = room.getHabboItem(Integer.parseInt(s)); - if (item != null) { - this.items.add(item); + if (item != null) { + this.items.add(item); + } + } catch (NumberFormatException ignored) { } } this.furniSource = this.items.isEmpty() ? WiredSourceUtil.SOURCE_TRIGGER : WiredSourceUtil.SOURCE_SELECTED; @@ -182,8 +208,8 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { if (count > Emulator.getConfig().getInt("hotel.wired.furni.selection.count")) return false; int[] params = settings.getIntParams(); - this.furniSource = (params.length > 0) ? params[0] : WiredSourceUtil.SOURCE_TRIGGER; - this.userSource = (params.length > 1) ? params[1] : WiredSourceUtil.SOURCE_TRIGGER; + this.furniSource = (params.length > 0) ? this.normalizeFurniSource(params[0]) : WiredSourceUtil.SOURCE_TRIGGER; + this.userSource = (params.length > 1) ? this.normalizeUserSource(params[1]) : WiredSourceUtil.SOURCE_TRIGGER; this.quantifier = (params.length > 2) ? this.normalizeQuantifier(params[2]) : QUANTIFIER_ALL; if (count > 0 && this.furniSource == WiredSourceUtil.SOURCE_TRIGGER) { @@ -233,6 +259,22 @@ public class WiredConditionTriggerOnFurni extends InteractionWiredCondition { return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; } + int normalizeFurniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + case WiredSourceUtil.SOURCE_TRIGGER: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + int normalizeUserSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + @Override public WiredConditionOperator operator() { return WiredConditionOperator.AND; diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniSelectionPayloadGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniSelectionPayloadGuardTest.java new file mode 100644 index 00000000..f4831443 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniSelectionPayloadGuardTest.java @@ -0,0 +1,37 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WiredConditionFurniSelectionPayloadGuardTest { + @Test + void furniHaveFurniBoundsFurniSources() { + WiredConditionFurniHaveFurni condition = new WiredConditionFurniHaveFurni(1, 1, null, "", 0, 0); + + assertEquals(WiredSourceUtil.SOURCE_SELECTED, condition.normalizeFurniSource(WiredSourceUtil.SOURCE_SELECTED)); + assertEquals(WiredSourceUtil.SOURCE_SELECTOR, condition.normalizeFurniSource(WiredSourceUtil.SOURCE_SELECTOR)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeFurniSource(-10)); + } + + @Test + void furniHaveHabboBoundsFurniSources() { + WiredConditionFurniHaveHabbo condition = new WiredConditionFurniHaveHabbo(1, 1, null, "", 0, 0); + + assertEquals(WiredSourceUtil.SOURCE_SIGNAL, condition.normalizeFurniSource(WiredSourceUtil.SOURCE_SIGNAL)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeFurniSource(8_000)); + } + + @Test + void triggerOnFurniBoundsFurniUserSourcesAndQuantifier() { + WiredConditionTriggerOnFurni condition = new WiredConditionTriggerOnFurni(1, 1, null, "", 0, 0); + + assertEquals(WiredSourceUtil.SOURCE_SELECTED, condition.normalizeFurniSource(WiredSourceUtil.SOURCE_SELECTED)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeFurniSource(77)); + assertEquals(WiredSourceUtil.SOURCE_CLICKED_USER, condition.normalizeUserSource(WiredSourceUtil.SOURCE_CLICKED_USER)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeUserSource(WiredSourceUtil.SOURCE_SELECTED)); + assertEquals(1, condition.normalizeQuantifier(1)); + assertEquals(0, condition.normalizeQuantifier(2)); + } +} From c83cd22fff0e2c035eae797c4a7909a07bd68ac9 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 19:55:04 +0200 Subject: [PATCH 38/43] fix(wired): bound variable payloads --- .../WiredConditionVariableAgeMatch.java | 25 +++++++---- .../WiredConditionVariableValueMatch.java | 32 +++++++++---- ...iredConditionVariablePayloadGuardTest.java | 45 +++++++++++++++++++ 3 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariablePayloadGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableAgeMatch.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableAgeMatch.java index 18456674..15012809 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableAgeMatch.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableAgeMatch.java @@ -35,6 +35,7 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable { private static final int DURATION_UNIT_WEEKS = 5; private static final int DURATION_UNIT_MONTHS = 6; private static final int DURATION_UNIT_YEARS = 7; + static final int MAX_DURATION_AMOUNT = 1_000_000; protected int compareValue = COMPARE_VALUE_CREATED; protected int comparison = COMPARISON_LOWER_THAN; @@ -97,7 +98,7 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable { this.targetType = (params.length > 0) ? normalizeTargetTypeExtended(params[0]) : TARGET_USER; this.compareValue = (params.length > 1) ? normalizeCompareValue(params[1]) : COMPARE_VALUE_CREATED; this.comparison = (params.length > 2) ? normalizeComparison(params[2]) : COMPARISON_LOWER_THAN; - this.durationAmount = Math.max(0, (params.length > 3) ? params[3] : 0); + this.durationAmount = normalizeDurationAmount((params.length > 3) ? params[3] : 0); this.durationUnit = (params.length > 4) ? normalizeDurationUnit(params[4]) : DURATION_UNIT_SECONDS; this.userSource = (params.length > 5) ? normalizeUserSource(params[5]) : WiredSourceUtil.SOURCE_TRIGGER; this.furniSource = (params.length > 6) ? normalizeFurniSource(params[6]) : WiredSourceUtil.SOURCE_TRIGGER; @@ -130,6 +131,10 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable { @Override public boolean evaluate(WiredContext ctx) { + if (ctx == null) { + return false; + } + Room room = ctx.room(); if (room == null || this.variableToken == null || this.variableToken.isEmpty() || !isCustomVariableToken(this.variableToken)) { @@ -192,7 +197,7 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable { this.targetType = normalizeTargetTypeExtended(data.targetType); this.compareValue = normalizeCompareValue(data.compareValue); this.comparison = normalizeComparison(data.comparison); - this.durationAmount = Math.max(0, data.durationAmount); + this.durationAmount = normalizeDurationAmount(data.durationAmount); this.durationUnit = normalizeDurationUnit(data.durationUnit); this.userSource = normalizeUserSource(data.userSource); this.furniSource = normalizeFurniSource(data.furniSource); @@ -345,8 +350,8 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable { return Math.max(0L, System.currentTimeMillis() - timestampMs); } - private static long durationToMillis(int amount, int unit) { - long normalizedAmount = Math.max(0L, amount); + static long durationToMillis(int amount, int unit) { + long normalizedAmount = normalizeDurationAmount(amount); return switch (unit) { case DURATION_UNIT_MILLISECONDS -> normalizedAmount; @@ -367,22 +372,26 @@ public class WiredConditionVariableAgeMatch extends WiredConditionHasVariable { return left * right; } - private static int normalizeTargetTypeExtended(int value) { + static int normalizeDurationAmount(int value) { + return Math.max(0, Math.min(MAX_DURATION_AMOUNT, value)); + } + + static int normalizeTargetTypeExtended(int value) { return switch (value) { case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; default -> TARGET_USER; }; } - private static int normalizeCompareValue(int value) { + static int normalizeCompareValue(int value) { return (value == COMPARE_VALUE_UPDATED) ? COMPARE_VALUE_UPDATED : COMPARE_VALUE_CREATED; } - private static int normalizeComparison(int value) { + static int normalizeComparison(int value) { return (value == COMPARISON_HIGHER_THAN) ? COMPARISON_HIGHER_THAN : COMPARISON_LOWER_THAN; } - private static int normalizeDurationUnit(int value) { + static int normalizeDurationUnit(int value) { return switch (value) { case DURATION_UNIT_MILLISECONDS, DURATION_UNIT_SECONDS, DURATION_UNIT_MINUTES, DURATION_UNIT_HOURS, DURATION_UNIT_DAYS, DURATION_UNIT_WEEKS, DURATION_UNIT_MONTHS, DURATION_UNIT_YEARS -> value; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableValueMatch.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableValueMatch.java index 13f79233..eb40e973 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableValueMatch.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariableValueMatch.java @@ -51,6 +51,7 @@ public class WiredConditionVariableValueMatch extends WiredConditionHasVariable private static final int COMPARISON_NOT_EQUAL = 5; private static final String DELIM = "\t"; private static final String FURNI_DELIM = ";"; + static final int MAX_ABS_REFERENCE_CONSTANT = 1_000_000_000; protected int comparison = COMPARISON_EQUAL; protected int referenceMode = REFERENCE_CONSTANT; @@ -123,7 +124,7 @@ public class WiredConditionVariableValueMatch extends WiredConditionHasVariable int nextTargetType = normalizeTargetTypeExtended(param(params, 0, TARGET_USER)); int nextComparison = normalizeComparison(param(params, 1, COMPARISON_EQUAL)); int nextReferenceMode = normalizeReferenceMode(param(params, 2, REFERENCE_CONSTANT)); - int nextReferenceConstantValue = param(params, 3, 0); + int nextReferenceConstantValue = normalizeReferenceConstantValue(param(params, 3, 0)); int nextReferenceTargetType = normalizeTargetTypeExtended(param(params, 4, TARGET_USER)); int nextUserSource = normalizeUserSource(param(params, 5, WiredSourceUtil.SOURCE_TRIGGER)); int nextFurniSource = normalizeFurniSource(param(params, 6, WiredSourceUtil.SOURCE_TRIGGER)); @@ -168,6 +169,10 @@ public class WiredConditionVariableValueMatch extends WiredConditionHasVariable @Override public boolean evaluate(WiredContext ctx) { + if (ctx == null) { + return false; + } + Room room = ctx.room(); if (room == null || this.variableToken == null || this.variableToken.isEmpty()) { @@ -220,14 +225,21 @@ public class WiredConditionVariableValueMatch extends WiredConditionHasVariable String wiredData = set.getString("wired_data"); if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) return; - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException exception) { + this.onPickUp(); + return; + } + if (data == null) return; this.targetType = normalizeTargetTypeExtended(data.targetType); this.setVariableToken(normalizeVariableToken((data.variableToken != null) ? data.variableToken : ((data.variableItemId > 0) ? String.valueOf(data.variableItemId) : ""))); this.comparison = normalizeComparison(data.comparison); this.referenceMode = normalizeReferenceMode(data.referenceMode); - this.referenceConstantValue = data.referenceConstantValue; + this.referenceConstantValue = normalizeReferenceConstantValue(data.referenceConstantValue); this.referenceTargetType = normalizeTargetTypeExtended(data.referenceTargetType); this.setReferenceVariableToken(normalizeVariableToken((data.referenceVariableToken != null) ? data.referenceVariableToken : ((data.referenceVariableItemId > 0) ? String.valueOf(data.referenceVariableItemId) : ""))); this.userSource = normalizeUserSource(data.userSource); @@ -737,32 +749,36 @@ public class WiredConditionVariableValueMatch extends WiredConditionHasVariable return (params.length > index) ? params[index] : fallback; } - private static int normalizeTargetTypeExtended(int value) { + static int normalizeTargetTypeExtended(int value) { return switch (value) { case TARGET_FURNI, TARGET_CONTEXT, TARGET_ROOM -> value; default -> TARGET_USER; }; } - private static int normalizeReferenceMode(int value) { + static int normalizeReferenceMode(int value) { return (value == REFERENCE_VARIABLE) ? REFERENCE_VARIABLE : REFERENCE_CONSTANT; } - private static int normalizeReferenceFurniSource(int value) { + static int normalizeReferenceFurniSource(int value) { return switch (value) { case SOURCE_SECONDARY_SELECTED, WiredSourceUtil.SOURCE_SELECTOR, WiredSourceUtil.SOURCE_SIGNAL -> value; default -> WiredSourceUtil.SOURCE_TRIGGER; }; } - private static int normalizeComparison(int value) { + static int normalizeComparison(int value) { return switch (value) { case COMPARISON_GREATER_THAN, COMPARISON_GREATER_THAN_OR_EQUAL, COMPARISON_LESS_THAN_OR_EQUAL, COMPARISON_LESS_THAN, COMPARISON_NOT_EQUAL -> value; default -> COMPARISON_EQUAL; }; } - private static int parseInteger(String value) { + static int normalizeReferenceConstantValue(int value) { + return Math.max(-MAX_ABS_REFERENCE_CONSTANT, Math.min(MAX_ABS_REFERENCE_CONSTANT, value)); + } + + static int parseInteger(String value) { try { return (value == null || value.trim().isEmpty()) ? 0 : Integer.parseInt(value.trim()); } catch (NumberFormatException e) { diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariablePayloadGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariablePayloadGuardTest.java new file mode 100644 index 00000000..eaecc7f3 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionVariablePayloadGuardTest.java @@ -0,0 +1,45 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WiredConditionVariablePayloadGuardTest { + @Test + void variableAgeDurationsAreBounded() { + assertEquals(0, WiredConditionVariableAgeMatch.normalizeDurationAmount(-1)); + assertEquals(42, WiredConditionVariableAgeMatch.normalizeDurationAmount(42)); + assertEquals(WiredConditionVariableAgeMatch.MAX_DURATION_AMOUNT, WiredConditionVariableAgeMatch.normalizeDurationAmount(Integer.MAX_VALUE)); + assertEquals(0L, WiredConditionVariableAgeMatch.durationToMillis(-1, 1)); + assertEquals(1_000L, WiredConditionVariableAgeMatch.durationToMillis(1, 1)); + } + + @Test + void variableAgeModesFallBackToSafeDefaults() { + assertEquals(0, WiredConditionVariableAgeMatch.normalizeTargetTypeExtended(999)); + assertEquals(1, WiredConditionVariableAgeMatch.normalizeTargetTypeExtended(1)); + assertEquals(0, WiredConditionVariableAgeMatch.normalizeCompareValue(99)); + assertEquals(1, WiredConditionVariableAgeMatch.normalizeCompareValue(1)); + assertEquals(0, WiredConditionVariableAgeMatch.normalizeComparison(99)); + assertEquals(2, WiredConditionVariableAgeMatch.normalizeComparison(2)); + assertEquals(1, WiredConditionVariableAgeMatch.normalizeDurationUnit(99)); + assertEquals(7, WiredConditionVariableAgeMatch.normalizeDurationUnit(7)); + } + + @Test + void variableValueModesAndConstantsAreBounded() { + assertEquals(0, WiredConditionVariableValueMatch.normalizeTargetTypeExtended(900)); + assertEquals(3, WiredConditionVariableValueMatch.normalizeTargetTypeExtended(3)); + assertEquals(0, WiredConditionVariableValueMatch.normalizeReferenceMode(5)); + assertEquals(1, WiredConditionVariableValueMatch.normalizeReferenceMode(1)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, WiredConditionVariableValueMatch.normalizeReferenceFurniSource(-1)); + assertEquals(101, WiredConditionVariableValueMatch.normalizeReferenceFurniSource(101)); + assertEquals(2, WiredConditionVariableValueMatch.normalizeComparison(99)); + assertEquals(5, WiredConditionVariableValueMatch.normalizeComparison(5)); + assertEquals(WiredConditionVariableValueMatch.MAX_ABS_REFERENCE_CONSTANT, WiredConditionVariableValueMatch.normalizeReferenceConstantValue(Integer.MAX_VALUE)); + assertEquals(-WiredConditionVariableValueMatch.MAX_ABS_REFERENCE_CONSTANT, WiredConditionVariableValueMatch.normalizeReferenceConstantValue(Integer.MIN_VALUE)); + assertEquals(0, WiredConditionVariableValueMatch.parseInteger("nope")); + assertEquals(123, WiredConditionVariableValueMatch.parseInteger(" 123 ")); + } +} From 23e6d4800af73b76bdbd9bcd8158ec102e98d1c5 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 20:00:12 +0200 Subject: [PATCH 39/43] fix(wired): bound date payloads --- .../WiredConditionDateRangeActive.java | 45 ++++++++++++++++--- .../conditions/WiredConditionMatchDate.java | 19 +++++--- .../conditions/WiredConditionMatchTime.java | 15 +++++-- .../com/eu/habbo/util/HotelDateTimeUtil.java | 4 +- .../WiredConditionDatePayloadGuardTest.java | 45 +++++++++++++++++++ 5 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionDatePayloadGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionDateRangeActive.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionDateRangeActive.java index 0b92d4e6..4ae14f56 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionDateRangeActive.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionDateRangeActive.java @@ -53,8 +53,7 @@ public class WiredConditionDateRangeActive extends InteractionWiredCondition { @Override public boolean saveData(WiredSettings settings) { if(settings.getIntParams().length < 2) return false; - this.startDate = settings.getIntParams()[0]; - this.endDate = settings.getIntParams()[1]; + this.setRange(settings.getIntParams()[0], settings.getIntParams()[1]); return true; } @@ -80,20 +79,34 @@ public class WiredConditionDateRangeActive extends InteractionWiredCondition { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.startDate = data.startDate; - this.endDate = data.endDate; + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException exception) { + this.onPickUp(); + return; + } + + if (data == null) { + return; + } + + this.setRange(data.startDate, data.endDate); } else { String[] data = wiredData.split("\t"); if (data.length == 2) { try { - this.startDate = Integer.parseInt(data[0]); - this.endDate = Integer.parseInt(data[1]); + this.setRange(Integer.parseInt(data[0]), Integer.parseInt(data[1])); } catch (Exception e) { + this.onPickUp(); } } } @@ -105,6 +118,24 @@ public class WiredConditionDateRangeActive extends InteractionWiredCondition { this.endDate = 0; } + void setRange(int startDate, int endDate) { + int normalizedStart = this.normalizeTimestamp(startDate); + int normalizedEnd = this.normalizeTimestamp(endDate); + + if (normalizedStart > normalizedEnd) { + this.startDate = normalizedEnd; + this.endDate = normalizedStart; + return; + } + + this.startDate = normalizedStart; + this.endDate = normalizedEnd; + } + + int normalizeTimestamp(int value) { + return Math.max(0, value); + } + static class JsonData { int startDate; int endDate; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchDate.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchDate.java index 3574480b..c214f266 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchDate.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchDate.java @@ -125,7 +125,14 @@ public class WiredConditionMatchDate extends InteractionWiredCondition { } if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException exception) { + this.reset(); + return; + } + if (data == null) { return; } @@ -193,7 +200,7 @@ public class WiredConditionMatchDate extends InteractionWiredCondition { } } - private int normalizeMode(int value) { + int normalizeMode(int value) { if (value < MODE_SKIP || value > MODE_RANGE) { return MODE_SKIP; } @@ -201,20 +208,20 @@ public class WiredConditionMatchDate extends InteractionWiredCondition { return value; } - private int normalizeDay(int value) { + int normalizeDay(int value) { return Math.max(1, Math.min(31, value)); } - private int normalizeYear(int value) { + int normalizeYear(int value) { return Math.max(1, Math.min(9999, value)); } - private int normalizeWeekdayMask(int value) { + int normalizeWeekdayMask(int value) { int normalized = value & ALL_WEEKDAYS_MASK; return (normalized == 0) ? ALL_WEEKDAYS_MASK : normalized; } - private int normalizeMonthMask(int value) { + int normalizeMonthMask(int value) { int normalized = value & ALL_MONTHS_MASK; return (normalized == 0) ? ALL_MONTHS_MASK : normalized; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchTime.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchTime.java index 982b56ec..69211d1a 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchTime.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchTime.java @@ -126,7 +126,14 @@ public class WiredConditionMatchTime extends InteractionWiredCondition { } if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException exception) { + this.reset(); + return; + } + if (data == null) { return; } @@ -195,7 +202,7 @@ public class WiredConditionMatchTime extends InteractionWiredCondition { } } - private int normalizeMode(int value) { + int normalizeMode(int value) { if (value < MODE_SKIP || value > MODE_RANGE) { return MODE_SKIP; } @@ -203,11 +210,11 @@ public class WiredConditionMatchTime extends InteractionWiredCondition { return value; } - private int normalizeHour(int value) { + int normalizeHour(int value) { return Math.max(0, Math.min(23, value)); } - private int normalizeMinuteOrSecond(int value) { + int normalizeMinuteOrSecond(int value) { return Math.max(0, Math.min(59, value)); } diff --git a/Emulator/src/main/java/com/eu/habbo/util/HotelDateTimeUtil.java b/Emulator/src/main/java/com/eu/habbo/util/HotelDateTimeUtil.java index 0abb37f3..3b5fbce0 100644 --- a/Emulator/src/main/java/com/eu/habbo/util/HotelDateTimeUtil.java +++ b/Emulator/src/main/java/com/eu/habbo/util/HotelDateTimeUtil.java @@ -23,7 +23,9 @@ public final class HotelDateTimeUtil { } public static ZoneId getZoneId() { - String configuredZoneId = Emulator.getConfig().getValue(CONFIG_KEY, ZoneId.systemDefault().getId()); + String configuredZoneId = Emulator.getConfig() != null + ? Emulator.getConfig().getValue(CONFIG_KEY, ZoneId.systemDefault().getId()) + : ZoneId.systemDefault().getId(); try { lastInvalidTimezoneId = null; diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionDatePayloadGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionDatePayloadGuardTest.java new file mode 100644 index 00000000..41e3317a --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionDatePayloadGuardTest.java @@ -0,0 +1,45 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WiredConditionDatePayloadGuardTest { + @Test + void matchDateBoundsMasksAndParts() { + WiredConditionMatchDate condition = new WiredConditionMatchDate(1, 1, null, "", 0, 0); + + assertEquals(0, condition.normalizeMode(99)); + assertEquals(2, condition.normalizeMode(2)); + assertEquals(1, condition.normalizeDay(-1)); + assertEquals(31, condition.normalizeDay(99)); + assertEquals(1, condition.normalizeYear(-1)); + assertEquals(9999, condition.normalizeYear(50_000)); + assertEquals(254, condition.normalizeWeekdayMask(0)); + assertEquals(8190, condition.normalizeMonthMask(0)); + } + + @Test + void matchTimeBoundsParts() { + WiredConditionMatchTime condition = new WiredConditionMatchTime(1, 1, null, "", 0, 0); + + assertEquals(0, condition.normalizeMode(99)); + assertEquals(2, condition.normalizeMode(2)); + assertEquals(0, condition.normalizeHour(-1)); + assertEquals(23, condition.normalizeHour(99)); + assertEquals(0, condition.normalizeMinuteOrSecond(-1)); + assertEquals(59, condition.normalizeMinuteOrSecond(99)); + } + + @Test + void dateRangeBoundsAndSortsUnixTimestamps() { + WiredConditionDateRangeActive condition = new WiredConditionDateRangeActive(1, 1, null, "", 0, 0); + + assertEquals(0, condition.normalizeTimestamp(-1)); + assertEquals(123, condition.normalizeTimestamp(123)); + + condition.setRange(200, 100); + + assertEquals("{\"startDate\":100,\"endDate\":200}", condition.getWiredData()); + } +} From 80c8467f82e69cd0d8dc6a06c8d16f857c7b1896 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 20:06:34 +0200 Subject: [PATCH 40/43] fix(wired): bound match payloads --- .../WiredConditionFurniTypeMatch.java | 19 +++- .../WiredConditionMatchStatePosition.java | 98 +++++++++++++++++-- .../WiredConditionNotFurniTypeMatch.java | 4 + .../WiredConditionNotMatchStatePosition.java | 4 + .../wired/WiredMatchFurniSetting.java | 2 +- .../WiredConditionMatchPayloadGuardTest.java | 56 +++++++++++ 6 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchPayloadGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniTypeMatch.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniTypeMatch.java index 02d2a0c9..42ee69c8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniTypeMatch.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionFurniTypeMatch.java @@ -52,6 +52,10 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition { @Override public boolean evaluate(WiredContext ctx) { + if (ctx == null) { + return false; + } + if (this.quantifier == QUANTIFIER_ANY) { return this.evaluateAnyMatches(ctx); } @@ -158,7 +162,14 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition { } if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException exception) { + this.onPickUp(); + return; + } + if (data == null) { return; } @@ -310,8 +321,8 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition { } } - private void loadItems(Room room, List itemIds, THashSet target) { - if (itemIds == null) { + void loadItems(Room room, List itemIds, THashSet target) { + if (room == null || itemIds == null || target == null) { return; } @@ -335,7 +346,7 @@ public class WiredConditionFurniTypeMatch extends InteractionWiredCondition { .collect(Collectors.joining(";")); } - private List parseIds(String value) { + List parseIds(String value) { List result = new ArrayList<>(); if (value == null || value.isEmpty()) { return result; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java index 36e721fc..b6050f44 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchStatePosition.java @@ -87,7 +87,7 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition this.direction = params[1] == 1; this.position = params[2] == 1; this.altitude = (params.length > 3) && (params[3] == 1); - this.furniSource = (params.length > 4) ? params[4] : ((params.length > 3 && params[3] > 1) ? params[3] : WiredSourceUtil.SOURCE_TRIGGER); + this.furniSource = (params.length > 4) ? this.normalizeFurniSource(params[4]) : ((params.length > 3 && params[3] > 1) ? this.normalizeFurniSource(params[3]) : WiredSourceUtil.SOURCE_TRIGGER); this.quantifier = (params.length > 5) ? this.normalizeQuantifier(params[5]) : QUANTIFIER_ALL; Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); @@ -113,6 +113,10 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition @Override public boolean evaluate(WiredContext ctx) { + if (ctx == null || ctx.room() == null) { + return false; + } + this.refresh(); if (this.settings.isEmpty()) @@ -126,6 +130,10 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition } protected boolean evaluateAllTargetsMatch(WiredContext ctx) { + if (ctx == null || ctx.room() == null) { + return false; + } + Room room = ctx.room(); if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { @@ -159,6 +167,10 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition } protected boolean evaluateAnyTargetMatches(WiredContext ctx) { + if (ctx == null || ctx.room() == null) { + return false; + } + Room room = ctx.room(); if (this.furniSource != WiredSourceUtil.SOURCE_SELECTED) { @@ -247,18 +259,38 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { + this.onPickUp(); String wiredData = set.getString("wired_data"); + if (wiredData == null || wiredData.isEmpty()) { + return; + } if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + JsonData data; + try { + data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + } catch (RuntimeException exception) { + this.onPickUp(); + return; + } + + if (data == null) { + return; + } + this.state = data.state; this.position = data.position; this.direction = data.direction; this.altitude = data.altitude; if (data.settings != null) { - this.settings.addAll(data.settings); + for (WiredMatchFurniSetting setting : data.settings) { + WiredMatchFurniSetting normalized = this.normalizeSetting(setting); + if (normalized != null) { + this.settings.add(normalized); + } + } } - this.furniSource = data.furniSource; + this.furniSource = this.normalizeFurniSource(data.furniSource); this.quantifier = this.normalizeQuantifier(data.quantifier); } else { String[] data = wiredData.split(":"); @@ -272,10 +304,10 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition for (int i = 0; i < itemCount && i < items.length; i++) { String[] stuff = items[i].split("-"); - if (stuff.length >= 6) - this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4]), Double.parseDouble(stuff[5]))); - else if (stuff.length >= 5) - this.settings.add(new WiredMatchFurniSetting(Integer.parseInt(stuff[0]), stuff[1], Integer.parseInt(stuff[2]), Integer.parseInt(stuff[3]), Integer.parseInt(stuff[4]))); + WiredMatchFurniSetting parsed = this.parseLegacySetting(stuff); + if (parsed != null) { + this.settings.add(parsed); + } } this.state = data[2].equals("1"); @@ -303,10 +335,58 @@ public class WiredConditionMatchStatePosition extends InteractionWiredCondition this.quantifier = QUANTIFIER_ALL; } - private int normalizeQuantifier(int value) { + int normalizeQuantifier(int value) { return (value == QUANTIFIER_ANY) ? QUANTIFIER_ANY : QUANTIFIER_ALL; } + int normalizeFurniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_TRIGGER: + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + WiredMatchFurniSetting normalizeSetting(WiredMatchFurniSetting setting) { + if (setting == null || setting.item_id <= 0) { + return null; + } + + int rotation = Math.max(0, Math.min(7, setting.rotation)); + int x = Math.max(0, setting.x); + int y = Math.max(0, setting.y); + double z = Math.max(0.0D, Math.min(Room.MAXIMUM_FURNI_HEIGHT, setting.z)); + + return new WiredMatchFurniSetting(setting.item_id, setting.state, rotation, x, y, z); + } + + WiredMatchFurniSetting parseLegacySetting(String[] values) { + if (values == null || values.length < 5) { + return null; + } + + try { + int itemId = Integer.parseInt(values[0]); + if (itemId <= 0) { + return null; + } + + String state = values[1]; + int rotation = Integer.parseInt(values[2]); + int x = Integer.parseInt(values[3]); + int y = Integer.parseInt(values[4]); + double z = values.length >= 6 ? Double.parseDouble(values[5]) : 0.0D; + + return this.normalizeSetting(new WiredMatchFurniSetting(itemId, state, rotation, x, y, z)); + } catch (RuntimeException exception) { + return null; + } + } + protected void refresh() { Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.getRoomId()); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniTypeMatch.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniTypeMatch.java index 49dc6775..501a7560 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniTypeMatch.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotFurniTypeMatch.java @@ -20,6 +20,10 @@ public class WiredConditionNotFurniTypeMatch extends WiredConditionFurniTypeMatc @Override public boolean evaluate(WiredContext ctx) { + if (ctx == null) { + return false; + } + if (this.getQuantifier() == QUANTIFIER_ANY) { return !this.evaluateAllMatches(ctx); } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotMatchStatePosition.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotMatchStatePosition.java index e52315dc..8fcaa996 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotMatchStatePosition.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionNotMatchStatePosition.java @@ -22,6 +22,10 @@ public class WiredConditionNotMatchStatePosition extends WiredConditionMatchStat @Override public boolean evaluate(WiredContext ctx) { + if (ctx == null || ctx.room() == null) { + return false; + } + this.refresh(); if (this.getMatchFurniSettings().isEmpty()) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredMatchFurniSetting.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredMatchFurniSetting.java index 5179e503..241974f0 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredMatchFurniSetting.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/wired/WiredMatchFurniSetting.java @@ -14,7 +14,7 @@ public class WiredMatchFurniSetting { public WiredMatchFurniSetting(int itemId, String state, int rotation, int x, int y, double z) { this.item_id = itemId; - this.state = state.replace("\t\t\t", " "); + this.state = state == null ? "" : state.replace("\t\t\t", " "); this.rotation = rotation; this.x = x; this.y = y; diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchPayloadGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchPayloadGuardTest.java new file mode 100644 index 00000000..e667ae73 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/conditions/WiredConditionMatchPayloadGuardTest.java @@ -0,0 +1,56 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.conditions; + +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.wired.WiredMatchFurniSetting; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +class WiredConditionMatchPayloadGuardTest { + @Test + void matchStateNormalizesSourcesQuantifierAndSettings() { + WiredConditionMatchStatePosition condition = new WiredConditionMatchStatePosition(1, 1, null, "", 0, 0); + + assertEquals(WiredSourceUtil.SOURCE_SELECTED, condition.normalizeFurniSource(WiredSourceUtil.SOURCE_SELECTED)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeFurniSource(9090)); + assertEquals(1, condition.normalizeQuantifier(1)); + assertEquals(0, condition.normalizeQuantifier(9)); + + WiredMatchFurniSetting normalized = condition.normalizeSetting(new WiredMatchFurniSetting(5, null, 99, -10, -20, Room.MAXIMUM_FURNI_HEIGHT + 100)); + + assertNotNull(normalized); + assertEquals(5, normalized.item_id); + assertEquals("", normalized.state); + assertEquals(7, normalized.rotation); + assertEquals(0, normalized.x); + assertEquals(0, normalized.y); + assertEquals(Room.MAXIMUM_FURNI_HEIGHT, normalized.z); + assertNull(condition.normalizeSetting(new WiredMatchFurniSetting(0, "", 0, 0, 0, 0))); + } + + @Test + void matchStateParsesLegacySettingsSafely() { + WiredConditionMatchStatePosition condition = new WiredConditionMatchStatePosition(1, 1, null, "", 0, 0); + + assertNotNull(condition.parseLegacySetting(new String[]{"7", "1", "2", "3", "4", "5"})); + assertNull(condition.parseLegacySetting(new String[]{"bad", "1", "2", "3", "4"})); + assertNull(condition.parseLegacySetting(new String[]{"7", "1"})); + } + + @Test + void furniTypeMatchBoundsSourcesAndParsesIds() { + WiredConditionFurniTypeMatch condition = new WiredConditionFurniTypeMatch(1, 1, null, "", 0, 0); + + assertEquals(WiredSourceUtil.SOURCE_SIGNAL, condition.normalizeFurniSource(WiredSourceUtil.SOURCE_SIGNAL)); + assertEquals(WiredConditionFurniTypeMatch.SOURCE_SECONDARY_SELECTED, condition.normalizeFurniSource(WiredConditionFurniTypeMatch.SOURCE_SECONDARY_SELECTED)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, condition.normalizeFurniSource(-77)); + assertEquals(1, condition.normalizeQuantifier(1)); + assertEquals(0, condition.normalizeQuantifier(6)); + assertEquals(List.of(1, 2, 3), condition.parseIds("1;bad,2\t3")); + } +} From fbe9eddb37ee513949c73869766d163b2e945378 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 20:13:54 +0200 Subject: [PATCH 41/43] fix(wired): bound trigger payloads --- .../wired/triggers/WiredTriggerAtSetTime.java | 53 ++++++++++++++----- .../triggers/WiredTriggerAtTimeLong.java | 53 ++++++++++++++----- .../wired/triggers/WiredTriggerRepeater.java | 50 +++++++++++------ .../triggers/WiredTriggerRepeaterLong.java | 48 ++++++++++++----- .../triggers/WiredTriggerScoreAchieved.java | 41 ++++++++++---- .../WiredTriggerPayloadGuardTest.java | 52 ++++++++++++++++++ 6 files changed, 228 insertions(+), 69 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerPayloadGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtSetTime.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtSetTime.java index 9f460464..4df86353 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtSetTime.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtSetTime.java @@ -29,6 +29,9 @@ import java.util.List; */ public class WiredTriggerAtSetTime extends InteractionWiredTrigger implements WiredTickable, WiredTriggerReset { public static final WiredTriggerType type = WiredTriggerType.AT_GIVEN_TIME; + static final int DEFAULT_EXECUTE_TIME = 20 * 500; + static final int MIN_EXECUTE_TIME = 500; + static final int MAX_EXECUTE_TIME = 60 * 60 * 1000; /** The time in milliseconds until the trigger fires */ public int executeTime; @@ -67,25 +70,38 @@ public class WiredTriggerAtSetTime extends InteractionWiredTrigger implements Wi @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); + this.executeTime = parseExecuteTime(wiredData); - if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.executeTime = data.executeTime; - } else { - if (wiredData.length() >= 1) { - this.executeTime = (Integer.parseInt(wiredData)); - } - } - - if (this.executeTime < 500) { - this.executeTime = 20 * 500; - } - // Initialize for tick system - will be registered by RoomItemManager this.accumulatedTime = 0; this.hasFired = false; } + static int parseExecuteTime(String wiredData) { + if (wiredData == null || wiredData.isBlank()) { + return DEFAULT_EXECUTE_TIME; + } + + try { + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + return clampExecuteTime(data != null ? data.executeTime : DEFAULT_EXECUTE_TIME); + } + + return clampExecuteTime(Integer.parseInt(wiredData)); + } catch (RuntimeException e) { + return DEFAULT_EXECUTE_TIME; + } + } + + static int clampExecuteTime(int executeTime) { + if (executeTime < MIN_EXECUTE_TIME) { + return DEFAULT_EXECUTE_TIME; + } + + return Math.min(executeTime, MAX_EXECUTE_TIME); + } + @Override public void onPickUp() { this.executeTime = 0; @@ -134,13 +150,22 @@ public class WiredTriggerAtSetTime extends InteractionWiredTrigger implements Wi @Override public boolean saveData(WiredSettings settings) { if (settings.getIntParams().length < 1) return false; - this.executeTime = settings.getIntParams()[0] * 500; + this.executeTime = clampExecuteTime(safeMultiply(settings.getIntParams()[0], MIN_EXECUTE_TIME)); this.resetTimer(); return true; } + private static int safeMultiply(int value, int factor) { + if (value <= 0) { + return DEFAULT_EXECUTE_TIME; + } + + long multiplied = (long) value * factor; + return multiplied > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) multiplied; + } + // ========== WiredTickable Implementation ========== @Override diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtTimeLong.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtTimeLong.java index 8864f4c3..194345d1 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtTimeLong.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerAtTimeLong.java @@ -29,6 +29,9 @@ import java.util.List; */ public class WiredTriggerAtTimeLong extends InteractionWiredTrigger implements WiredTickable, WiredTriggerReset { private static final WiredTriggerType type = WiredTriggerType.AT_GIVEN_TIME; + static final int DEFAULT_EXECUTE_TIME = 20 * 500; + static final int MIN_EXECUTE_TIME = 500; + static final int MAX_EXECUTE_TIME = 60 * 60 * 1000; /** The time in milliseconds until the trigger fires */ private int executeTime; @@ -67,25 +70,38 @@ public class WiredTriggerAtTimeLong extends InteractionWiredTrigger implements W @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); + this.executeTime = parseExecuteTime(wiredData); - if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.executeTime = data.executeTime; - } else { - if (wiredData.length() >= 1) { - this.executeTime = (Integer.parseInt(wiredData)); - } - } - - if (this.executeTime < 500) { - this.executeTime = 20 * 500; - } - // Initialize for tick system this.accumulatedTime = 0; this.hasFired = false; } + static int parseExecuteTime(String wiredData) { + if (wiredData == null || wiredData.isBlank()) { + return DEFAULT_EXECUTE_TIME; + } + + try { + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + return clampExecuteTime(data != null ? data.executeTime : DEFAULT_EXECUTE_TIME); + } + + return clampExecuteTime(Integer.parseInt(wiredData)); + } catch (RuntimeException e) { + return DEFAULT_EXECUTE_TIME; + } + } + + static int clampExecuteTime(int executeTime) { + if (executeTime < MIN_EXECUTE_TIME) { + return DEFAULT_EXECUTE_TIME; + } + + return Math.min(executeTime, MAX_EXECUTE_TIME); + } + @Override public void onPickUp() { this.executeTime = 0; @@ -134,13 +150,22 @@ public class WiredTriggerAtTimeLong extends InteractionWiredTrigger implements W @Override public boolean saveData(WiredSettings settings) { if (settings.getIntParams().length < 1) return false; - this.executeTime = settings.getIntParams()[0] * 500; + this.executeTime = clampExecuteTime(safeMultiply(settings.getIntParams()[0], MIN_EXECUTE_TIME)); this.resetTimer(); return true; } + private static int safeMultiply(int value, int factor) { + if (value <= 0) { + return DEFAULT_EXECUTE_TIME; + } + + long multiplied = (long) value * factor; + return multiplied > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) multiplied; + } + // ========== WiredTickable Implementation ========== @Override diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeater.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeater.java index 2ac70e88..6acfedf0 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeater.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeater.java @@ -30,6 +30,8 @@ import java.util.List; public class WiredTriggerRepeater extends InteractionWiredTrigger implements WiredTickable, WiredTriggerReset { public static final WiredTriggerType type = WiredTriggerType.PERIODICALLY; public static final int DEFAULT_DELAY = 10 * 500; // 5 seconds default + static final int MIN_DELAY = 500; + static final int MAX_DELAY = 60 * 60 * 1000; /** The interval in milliseconds between triggers */ protected int repeatTime = DEFAULT_DELAY; @@ -62,19 +64,32 @@ public class WiredTriggerRepeater extends InteractionWiredTrigger implements Wir @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); + this.repeatTime = parseRepeatTime(wiredData); + } - if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.repeatTime = data.repeatTime; - } else { - if (wiredData.length() >= 1) { - this.repeatTime = (Integer.parseInt(wiredData)); + static int parseRepeatTime(String wiredData) { + if (wiredData == null || wiredData.isBlank()) { + return DEFAULT_DELAY; + } + + try { + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + return normalizeRepeatTime(data != null ? data.repeatTime : DEFAULT_DELAY); } + + return normalizeRepeatTime(Integer.parseInt(wiredData)); + } catch (RuntimeException e) { + return DEFAULT_DELAY; + } + } + + static int normalizeRepeatTime(int repeatTime) { + if (repeatTime < MIN_DELAY) { + return DEFAULT_DELAY; } - if (this.repeatTime < 500) { - this.repeatTime = 20 * 500; - } + return Math.min(repeatTime, MAX_DELAY); } @Override @@ -123,17 +138,20 @@ public class WiredTriggerRepeater extends InteractionWiredTrigger implements Wir @Override public boolean saveData(WiredSettings settings) { if (settings.getIntParams().length < 1) return false; - int newRepeatTime = settings.getIntParams()[0] * 500; - - if (newRepeatTime < 500) { - newRepeatTime = 500; - } - - this.repeatTime = newRepeatTime; + this.repeatTime = normalizeRepeatTime(safeMultiply(settings.getIntParams()[0], MIN_DELAY)); return true; } + private static int safeMultiply(int value, int factor) { + if (value <= 0) { + return DEFAULT_DELAY; + } + + long multiplied = (long) value * factor; + return multiplied > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) multiplied; + } + // ========== WiredTickable Implementation ========== @Override diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterLong.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterLong.java index ffc1d92f..1e37d5e2 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterLong.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerRepeaterLong.java @@ -29,6 +29,8 @@ import java.util.List; */ public class WiredTriggerRepeaterLong extends InteractionWiredTrigger implements WiredTickable, WiredTriggerReset { public static final int DEFAULT_DELAY = 10 * 5000; // 50 seconds default + static final int MIN_DELAY = 5000; + static final int MAX_DELAY = 60 * 60 * 1000; private static final WiredTriggerType type = WiredTriggerType.PERIODICALLY_LONG; /** The interval in milliseconds between triggers */ @@ -62,19 +64,32 @@ public class WiredTriggerRepeaterLong extends InteractionWiredTrigger implements @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); + this.repeatTime = parseRepeatTime(wiredData); + } - if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.repeatTime = data.repeatTime; - } else { - if (wiredData.length() >= 1) { - this.repeatTime = (Integer.parseInt(wiredData)); + static int parseRepeatTime(String wiredData) { + if (wiredData == null || wiredData.isBlank()) { + return DEFAULT_DELAY; + } + + try { + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + return clampRepeatTime(data != null ? data.repeatTime : DEFAULT_DELAY); } + + return clampRepeatTime(Integer.parseInt(wiredData)); + } catch (RuntimeException e) { + return DEFAULT_DELAY; + } + } + + static int clampRepeatTime(int repeatTime) { + if (repeatTime < MIN_DELAY) { + return DEFAULT_DELAY; } - if (this.repeatTime < 5000) { - this.repeatTime = 20 * 5000; - } + return Math.min(repeatTime, MAX_DELAY); } @Override @@ -123,15 +138,20 @@ public class WiredTriggerRepeaterLong extends InteractionWiredTrigger implements @Override public boolean saveData(WiredSettings settings) { if (settings.getIntParams().length < 1) return false; - int interval = settings.getIntParams()[0]; - if (interval < 1) { - interval = 1; - } - this.repeatTime = interval * 5000; + this.repeatTime = clampRepeatTime(safeMultiply(settings.getIntParams()[0], MIN_DELAY)); // No accumulated time reset needed - using global tick count return true; } + private static int safeMultiply(int value, int factor) { + if (value <= 0) { + return DEFAULT_DELAY; + } + + long multiplied = (long) value * factor; + return multiplied > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) multiplied; + } + // ========== WiredTickable Implementation ========== @Override diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerScoreAchieved.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerScoreAchieved.java index 8bf9c7b2..ad31d0d8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerScoreAchieved.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerScoreAchieved.java @@ -18,6 +18,7 @@ import java.sql.SQLException; public class WiredTriggerScoreAchieved extends InteractionWiredTrigger { private static final WiredTriggerType type = WiredTriggerType.SCORE_ACHIEVED; + static final int MAX_SCORE = 1_000_000; private int score = 0; private int teamType = GameTeamColors.NONE.type; @@ -71,17 +72,27 @@ public class WiredTriggerScoreAchieved extends InteractionWiredTrigger { @Override public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); + JsonData data = parseData(wiredData); + this.score = data.score; + this.teamType = data.teamType; + } - if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.score = data.score; - this.teamType = normalizeTeamType(data.teamType); - } else { - try { - this.score = Integer.parseInt(wiredData); - } catch (Exception e) { + static JsonData parseData(String wiredData) { + if (wiredData == null || wiredData.isBlank()) { + return new JsonData(0, GameTeamColors.NONE.type); + } + + try { + if (wiredData.startsWith("{")) { + JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + return data != null + ? new JsonData(clampScore(data.score), normalizeTeamType(data.teamType)) + : new JsonData(0, GameTeamColors.NONE.type); } - this.teamType = GameTeamColors.NONE.type; + + return new JsonData(clampScore(Integer.parseInt(wiredData)), GameTeamColors.NONE.type); + } catch (RuntimeException e) { + return new JsonData(0, GameTeamColors.NONE.type); } } @@ -116,7 +127,7 @@ public class WiredTriggerScoreAchieved extends InteractionWiredTrigger { @Override public boolean saveData(WiredSettings settings) { if(settings.getIntParams().length < 1) return false; - this.score = settings.getIntParams()[0]; + this.score = clampScore(settings.getIntParams()[0]); this.teamType = (settings.getIntParams().length > 1) ? normalizeTeamType(settings.getIntParams()[1]) : GameTeamColors.NONE.type; @@ -128,7 +139,15 @@ public class WiredTriggerScoreAchieved extends InteractionWiredTrigger { return true; } - private int normalizeTeamType(int value) { + static int clampScore(int value) { + if (value < 0) { + return 0; + } + + return Math.min(value, MAX_SCORE); + } + + static int normalizeTeamType(int value) { if (value >= GameTeamColors.RED.type && value <= GameTeamColors.YELLOW.type) { return value; } diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerPayloadGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerPayloadGuardTest.java new file mode 100644 index 00000000..c350c335 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/triggers/WiredTriggerPayloadGuardTest.java @@ -0,0 +1,52 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.triggers; + +import com.eu.habbo.habbohotel.games.GameTeamColors; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WiredTriggerPayloadGuardTest { + @Test + void repeaterPayloadsFallBackOnInvalidDataAndClampUpperBound() { + assertEquals(WiredTriggerRepeater.DEFAULT_DELAY, WiredTriggerRepeater.parseRepeatTime(null)); + assertEquals(WiredTriggerRepeater.DEFAULT_DELAY, WiredTriggerRepeater.parseRepeatTime("not-a-number")); + assertEquals(WiredTriggerRepeater.DEFAULT_DELAY, WiredTriggerRepeater.parseRepeatTime("{broken")); + assertEquals(WiredTriggerRepeater.DEFAULT_DELAY, WiredTriggerRepeater.parseRepeatTime("{\"repeatTime\":0}")); + assertEquals(WiredTriggerRepeater.MAX_DELAY, WiredTriggerRepeater.parseRepeatTime("{\"repeatTime\":2147483647}")); + + assertEquals(WiredTriggerRepeaterLong.DEFAULT_DELAY, WiredTriggerRepeaterLong.parseRepeatTime(null)); + assertEquals(WiredTriggerRepeaterLong.DEFAULT_DELAY, WiredTriggerRepeaterLong.parseRepeatTime("1")); + assertEquals(WiredTriggerRepeaterLong.MAX_DELAY, WiredTriggerRepeaterLong.parseRepeatTime("2147483647")); + } + + @Test + void atTimePayloadsFallBackOnInvalidDataAndClampUpperBound() { + assertEquals(WiredTriggerAtSetTime.DEFAULT_EXECUTE_TIME, WiredTriggerAtSetTime.parseExecuteTime(null)); + assertEquals(WiredTriggerAtSetTime.DEFAULT_EXECUTE_TIME, WiredTriggerAtSetTime.parseExecuteTime("bad")); + assertEquals(WiredTriggerAtSetTime.DEFAULT_EXECUTE_TIME, WiredTriggerAtSetTime.parseExecuteTime("{\"executeTime\":0}")); + assertEquals(WiredTriggerAtSetTime.MAX_EXECUTE_TIME, WiredTriggerAtSetTime.parseExecuteTime("{\"executeTime\":2147483647}")); + + assertEquals(WiredTriggerAtTimeLong.DEFAULT_EXECUTE_TIME, WiredTriggerAtTimeLong.parseExecuteTime("{broken")); + assertEquals(WiredTriggerAtTimeLong.DEFAULT_EXECUTE_TIME, WiredTriggerAtTimeLong.parseExecuteTime("1")); + assertEquals(WiredTriggerAtTimeLong.MAX_EXECUTE_TIME, WiredTriggerAtTimeLong.parseExecuteTime("2147483647")); + } + + @Test + void scorePayloadsNormalizeScoreAndTeam() { + WiredTriggerScoreAchieved.JsonData invalid = WiredTriggerScoreAchieved.parseData("{broken"); + assertEquals(0, invalid.score); + assertEquals(GameTeamColors.NONE.type, invalid.teamType); + + WiredTriggerScoreAchieved.JsonData legacy = WiredTriggerScoreAchieved.parseData("-10"); + assertEquals(0, legacy.score); + assertEquals(GameTeamColors.NONE.type, legacy.teamType); + + WiredTriggerScoreAchieved.JsonData capped = WiredTriggerScoreAchieved.parseData("{\"score\":2147483647,\"teamType\":999}"); + assertEquals(WiredTriggerScoreAchieved.MAX_SCORE, capped.score); + assertEquals(GameTeamColors.NONE.type, capped.teamType); + + WiredTriggerScoreAchieved.JsonData validTeam = WiredTriggerScoreAchieved.parseData("{\"score\":50,\"teamType\":" + GameTeamColors.RED.type + "}"); + assertEquals(50, validTeam.score); + assertEquals(GameTeamColors.RED.type, validTeam.teamType); + } +} From 60b998f90905bfee2dd28b8d4259308565f0a9e5 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 20:18:48 +0200 Subject: [PATCH 42/43] fix(wired): bound bot effect payloads --- .../wired/effects/WiredEffectBotClothes.java | 20 +++--- .../effects/WiredEffectBotFollowHabbo.java | 24 +++---- .../effects/WiredEffectBotGiveHandItem.java | 24 +++---- .../wired/effects/WiredEffectBotTalk.java | 24 +++---- .../effects/WiredEffectBotTalkToHabbo.java | 24 +++---- .../wired/effects/WiredEffectBotTeleport.java | 33 ++++----- .../effects/WiredEffectBotWalkToFurni.java | 33 ++++----- .../effects/WiredEffectPayloadGuard.java | 67 +++++++++++++++++++ .../effects/WiredEffectPayloadGuardTest.java | 35 ++++++++++ 9 files changed, 196 insertions(+), 88 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectPayloadGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectPayloadGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotClothes.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotClothes.java index 345fbb0c..998d533b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotClothes.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotClothes.java @@ -106,21 +106,21 @@ public class WiredEffectBotClothes extends InteractionWiredEffect { public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); - if(wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.setDelay(data.delay); - this.botName = data.bot_name; - this.botLook = data.look; - this.botSource = (data.botSource != null) - ? WiredBotSourceUtil.normalizeBotSource(data.botSource) + JsonData jsonData = WiredEffectPayloadGuard.fromJson(wiredData, JsonData.class); + if(jsonData != null) { + this.setDelay(WiredEffectPayloadGuard.delay(jsonData.delay)); + this.botName = WiredEffectPayloadGuard.text(jsonData.bot_name); + this.botLook = jsonData.look != null ? jsonData.look : ""; + this.botSource = (jsonData.botSource != null) + ? WiredBotSourceUtil.normalizeBotSource(jsonData.botSource) : WiredBotSourceUtil.SOURCE_BOT_NAME; } else { - String[] data = wiredData.split(((char) 9) + ""); + String[] data = wiredData != null ? wiredData.split(((char) 9) + "") : new String[0]; if (data.length >= 3) { - this.setDelay(Integer.parseInt(data[0])); - this.botName = data[1]; + this.setDelay(WiredEffectPayloadGuard.parseDelay(data[0])); + this.botName = WiredEffectPayloadGuard.text(data[1]); this.botLook = data[2]; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotFollowHabbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotFollowHabbo.java index a1a80fe7..0d7bfb5f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotFollowHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotFollowHabbo.java @@ -143,23 +143,23 @@ public class WiredEffectBotFollowHabbo extends InteractionWiredEffect { public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); - if(wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.setDelay(data.delay); - this.mode = data.mode; - this.botName = data.bot_name; - this.userSource = data.userSource; - this.botSource = (data.botSource != null) - ? WiredBotSourceUtil.normalizeBotSource(data.botSource) + JsonData jsonData = WiredEffectPayloadGuard.fromJson(wiredData, JsonData.class); + if(jsonData != null) { + this.setDelay(WiredEffectPayloadGuard.delay(jsonData.delay)); + this.mode = WiredEffectPayloadGuard.mode(jsonData.mode); + this.botName = WiredEffectPayloadGuard.text(jsonData.bot_name); + this.userSource = WiredSourceUtil.isDefaultUserSource(jsonData.userSource) ? jsonData.userSource : WiredSourceUtil.SOURCE_TRIGGER; + this.botSource = (jsonData.botSource != null) + ? WiredBotSourceUtil.normalizeBotSource(jsonData.botSource) : WiredBotSourceUtil.SOURCE_BOT_NAME; } else { - String[] data = wiredData.split(((char) 9) + ""); + String[] data = wiredData != null ? wiredData.split(((char) 9) + "") : new String[0]; if (data.length == 3) { - this.setDelay(Integer.parseInt(data[0])); - this.mode = (data[1].equalsIgnoreCase("1") ? 1 : 0); - this.botName = data[2]; + this.setDelay(WiredEffectPayloadGuard.parseDelay(data[0])); + this.mode = WiredEffectPayloadGuard.mode(WiredEffectPayloadGuard.parseInt(data[1], 0)); + this.botName = WiredEffectPayloadGuard.text(data[2]); } this.needsUpdate(true); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotGiveHandItem.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotGiveHandItem.java index 53a8be66..97e7505b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotGiveHandItem.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotGiveHandItem.java @@ -153,23 +153,23 @@ public class WiredEffectBotGiveHandItem extends InteractionWiredEffect { public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); - if(wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.setDelay(data.delay); - this.itemId = this.normalizeHandItem(data.item_id); - this.botName = data.bot_name; - this.userSource = this.normalizeUserSource(data.userSource); - this.botSource = ((data.botSource == WiredSourceUtil.SOURCE_TRIGGER) && this.botName != null && !this.botName.isEmpty()) + JsonData jsonData = WiredEffectPayloadGuard.fromJson(wiredData, JsonData.class); + if(jsonData != null) { + this.setDelay(WiredEffectPayloadGuard.delay(jsonData.delay)); + this.itemId = this.normalizeHandItem(jsonData.item_id); + this.botName = WiredEffectPayloadGuard.text(jsonData.bot_name); + this.userSource = this.normalizeUserSource(jsonData.userSource); + this.botSource = ((jsonData.botSource == WiredSourceUtil.SOURCE_TRIGGER) && this.botName != null && !this.botName.isEmpty()) ? BOT_SOURCE_NAME - : this.normalizeBotSource(data.botSource); + : this.normalizeBotSource(jsonData.botSource); } else { - String[] data = wiredData.split(((char) 9) + ""); + String[] data = wiredData != null ? wiredData.split(((char) 9) + "") : new String[0]; if (data.length == 3) { - this.setDelay(Integer.parseInt(data[0])); - this.itemId = this.normalizeHandItem(Integer.parseInt(data[1])); - this.botName = data[2]; + this.setDelay(WiredEffectPayloadGuard.parseDelay(data[0])); + this.itemId = this.normalizeHandItem(WiredEffectPayloadGuard.parseInt(data[1], 0)); + this.botName = WiredEffectPayloadGuard.text(data[2]); } this.needsUpdate(true); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalk.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalk.java index b4f20a8f..fb392171 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalk.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalk.java @@ -144,23 +144,23 @@ public class WiredEffectBotTalk extends InteractionWiredEffect { public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); - if(wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.setDelay(data.delay); - this.mode = data.mode; - this.botName = data.bot_name; - this.message = data.message; - this.botSource = (data.botSource != null) - ? WiredBotSourceUtil.normalizeBotSource(data.botSource) + JsonData jsonData = WiredEffectPayloadGuard.fromJson(wiredData, JsonData.class); + if(jsonData != null) { + this.setDelay(WiredEffectPayloadGuard.delay(jsonData.delay)); + this.mode = WiredEffectPayloadGuard.mode(jsonData.mode); + this.botName = WiredEffectPayloadGuard.text(jsonData.bot_name); + this.message = jsonData.message != null ? jsonData.message : ""; + this.botSource = (jsonData.botSource != null) + ? WiredBotSourceUtil.normalizeBotSource(jsonData.botSource) : WiredBotSourceUtil.SOURCE_BOT_NAME; } else { - String[] data = wiredData.split(((char) 9) + ""); + String[] data = wiredData != null ? wiredData.split(((char) 9) + "") : new String[0]; if (data.length == 4) { - this.setDelay(Integer.parseInt(data[0])); - this.mode = data[1].equalsIgnoreCase("1") ? 1 : 0; - this.botName = data[2]; + this.setDelay(WiredEffectPayloadGuard.parseDelay(data[0])); + this.mode = WiredEffectPayloadGuard.mode(WiredEffectPayloadGuard.parseInt(data[1], 0)); + this.botName = WiredEffectPayloadGuard.text(data[2]); this.message = data[3]; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalkToHabbo.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalkToHabbo.java index c3bc7dfe..7e854823 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalkToHabbo.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTalkToHabbo.java @@ -168,22 +168,22 @@ public class WiredEffectBotTalkToHabbo extends InteractionWiredEffect { public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); - if(wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.setDelay(data.delay); - this.mode = data.mode; - this.botName = data.bot_name; - this.message = data.message; - this.userSource = data.userSource; - this.botSource = (data.botSource != null) ? WiredBotSourceUtil.normalizeBotSource(data.botSource) : WiredBotSourceUtil.SOURCE_BOT_NAME; + JsonData jsonData = WiredEffectPayloadGuard.fromJson(wiredData, JsonData.class); + if(jsonData != null) { + this.setDelay(WiredEffectPayloadGuard.delay(jsonData.delay)); + this.mode = WiredEffectPayloadGuard.mode(jsonData.mode); + this.botName = WiredEffectPayloadGuard.text(jsonData.bot_name); + this.message = jsonData.message != null ? jsonData.message : ""; + this.userSource = WiredSourceUtil.isDefaultUserSource(jsonData.userSource) ? jsonData.userSource : WiredSourceUtil.SOURCE_TRIGGER; + this.botSource = (jsonData.botSource != null) ? WiredBotSourceUtil.normalizeBotSource(jsonData.botSource) : WiredBotSourceUtil.SOURCE_BOT_NAME; } else { - String[] data = wiredData.split(((char) 9) + ""); + String[] data = wiredData != null ? wiredData.split(((char) 9) + "") : new String[0]; if (data.length == 4) { - this.setDelay(Integer.parseInt(data[0])); - this.mode = data[1].equalsIgnoreCase("1") ? 1 : 0; - this.botName = data[2]; + this.setDelay(WiredEffectPayloadGuard.parseDelay(data[0])); + this.mode = WiredEffectPayloadGuard.mode(WiredEffectPayloadGuard.parseInt(data[1], 0)); + this.botName = WiredEffectPayloadGuard.text(data[2]); this.message = data[3]; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTeleport.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTeleport.java index cb7d6d22..49faad68 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTeleport.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotTeleport.java @@ -231,37 +231,40 @@ public class WiredEffectBotTeleport extends InteractionWiredEffect { String wiredData = set.getString("wired_data"); - if(wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.setDelay(data.delay); - this.botName = data.bot_name; - this.furniSource = data.furniSource; - this.botSource = (data.botSource != null) - ? WiredBotSourceUtil.normalizeBotSource(data.botSource) + JsonData jsonData = WiredEffectPayloadGuard.fromJson(wiredData, JsonData.class); + if(jsonData != null) { + this.setDelay(WiredEffectPayloadGuard.delay(jsonData.delay)); + this.botName = WiredEffectPayloadGuard.text(jsonData.bot_name); + this.furniSource = WiredEffectPayloadGuard.furniSource(jsonData.furniSource); + this.botSource = (jsonData.botSource != null) + ? WiredBotSourceUtil.normalizeBotSource(jsonData.botSource) : WiredBotSourceUtil.SOURCE_BOT_NAME; - for(int itemId : data.items) { - HabboItem item = room.getHabboItem(itemId); + if (jsonData.items != null) { + for(int itemId : jsonData.items) { + HabboItem item = room.getHabboItem(itemId); - if (item != null) - this.items.add(item); + if (item != null) + this.items.add(item); + } } if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { this.furniSource = WiredSourceUtil.SOURCE_SELECTED; } } else { - String[] wiredDataSplit = set.getString("wired_data").split("\t"); + String[] wiredDataSplit = wiredData != null ? wiredData.split("\t") : new String[0]; if (wiredDataSplit.length >= 2) { - this.setDelay(Integer.parseInt(wiredDataSplit[0])); + this.setDelay(WiredEffectPayloadGuard.parseDelay(wiredDataSplit[0])); String[] data = wiredDataSplit[1].split(";"); if (data.length > 1) { - this.botName = data[0]; + this.botName = WiredEffectPayloadGuard.text(data[0]); for (int i = 1; i < data.length; i++) { - HabboItem item = room.getHabboItem(Integer.parseInt(data[i])); + int itemId = WiredEffectPayloadGuard.parseInt(data[i], 0); + HabboItem item = itemId > 0 ? room.getHabboItem(itemId) : null; if (item != null) this.items.add(item); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotWalkToFurni.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotWalkToFurni.java index 0ccb96b6..6f35f0d0 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotWalkToFurni.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectBotWalkToFurni.java @@ -181,37 +181,40 @@ public class WiredEffectBotWalkToFurni extends InteractionWiredEffect { String wiredData = set.getString("wired_data"); - if(wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.setDelay(data.delay); - this.botName = data.bot_name; - this.furniSource = data.furniSource; - this.botSource = (data.botSource != null) - ? WiredBotSourceUtil.normalizeBotSource(data.botSource) + JsonData jsonData = WiredEffectPayloadGuard.fromJson(wiredData, JsonData.class); + if(jsonData != null) { + this.setDelay(WiredEffectPayloadGuard.delay(jsonData.delay)); + this.botName = WiredEffectPayloadGuard.text(jsonData.bot_name); + this.furniSource = WiredEffectPayloadGuard.furniSource(jsonData.furniSource); + this.botSource = (jsonData.botSource != null) + ? WiredBotSourceUtil.normalizeBotSource(jsonData.botSource) : WiredBotSourceUtil.SOURCE_BOT_NAME; - for(int itemId : data.items) { - HabboItem item = room.getHabboItem(itemId); + if (jsonData.items != null) { + for(int itemId : jsonData.items) { + HabboItem item = room.getHabboItem(itemId); - if (item != null) - this.items.add(item); + if (item != null) + this.items.add(item); + } } if (this.furniSource == WiredSourceUtil.SOURCE_TRIGGER && !this.items.isEmpty()) { this.furniSource = WiredSourceUtil.SOURCE_SELECTED; } } else { - String[] wiredDataSplit = set.getString("wired_data").split("\t"); + String[] wiredDataSplit = wiredData != null ? wiredData.split("\t") : new String[0]; if (wiredDataSplit.length >= 2) { - this.setDelay(Integer.parseInt(wiredDataSplit[0])); + this.setDelay(WiredEffectPayloadGuard.parseDelay(wiredDataSplit[0])); String[] data = wiredDataSplit[1].split(";"); if (data.length > 1) { - this.botName = data[0]; + this.botName = WiredEffectPayloadGuard.text(data[0]); for (int i = 1; i < data.length; i++) { - HabboItem item = room.getHabboItem(Integer.parseInt(data[i])); + int itemId = WiredEffectPayloadGuard.parseInt(data[i], 0); + HabboItem item = itemId > 0 ? room.getHabboItem(itemId) : null; if (item != null) this.items.add(item); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectPayloadGuard.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectPayloadGuard.java new file mode 100644 index 00000000..09fdabc8 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectPayloadGuard.java @@ -0,0 +1,67 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; + +final class WiredEffectPayloadGuard { + static final int MAX_LOAD_DELAY = 3600; + + private WiredEffectPayloadGuard() { + } + + static int delay(int value) { + if (value < 0) { + return 0; + } + + return Math.min(value, MAX_LOAD_DELAY); + } + + static int parseDelay(String value) { + return delay(parseInt(value, 0)); + } + + static int parseInt(String value, int fallback) { + if (value == null) { + return fallback; + } + + try { + return Integer.parseInt(value.trim()); + } catch (RuntimeException e) { + return fallback; + } + } + + static int mode(int value) { + return value == 1 ? 1 : 0; + } + + static int furniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_TRIGGER: + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + static String text(String value) { + return value == null ? "" : value.replace("\t", ""); + } + + static T fromJson(String wiredData, Class type) { + if (wiredData == null || !wiredData.startsWith("{")) { + return null; + } + + try { + return WiredManager.getGson().fromJson(wiredData, type); + } catch (RuntimeException e) { + return null; + } + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectPayloadGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectPayloadGuardTest.java new file mode 100644 index 00000000..b1d1e5e0 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectPayloadGuardTest.java @@ -0,0 +1,35 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class WiredEffectPayloadGuardTest { + @Test + void delayIsBoundedForStoredPayloads() { + assertEquals(0, WiredEffectPayloadGuard.delay(-1)); + assertEquals(20, WiredEffectPayloadGuard.delay(20)); + assertEquals(WiredEffectPayloadGuard.MAX_LOAD_DELAY, WiredEffectPayloadGuard.delay(Integer.MAX_VALUE)); + assertEquals(0, WiredEffectPayloadGuard.parseDelay("bad")); + assertEquals(5, WiredEffectPayloadGuard.parseDelay(" 5 ")); + } + + @Test + void modeAndTextFallbacksAreSafe() { + assertEquals(0, WiredEffectPayloadGuard.mode(-1)); + assertEquals(0, WiredEffectPayloadGuard.mode(2)); + assertEquals(1, WiredEffectPayloadGuard.mode(1)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, WiredEffectPayloadGuard.furniSource(-1)); + assertEquals(WiredSourceUtil.SOURCE_SELECTED, WiredEffectPayloadGuard.furniSource(WiredSourceUtil.SOURCE_SELECTED)); + assertEquals("", WiredEffectPayloadGuard.text(null)); + assertEquals("botname", WiredEffectPayloadGuard.text("bot\tname")); + } + + @Test + void malformedJsonReturnsNullInsteadOfThrowing() { + assertNull(WiredEffectPayloadGuard.fromJson("{broken", WiredEffectBotTalk.JsonData.class)); + assertNull(WiredEffectPayloadGuard.fromJson(null, WiredEffectBotTalk.JsonData.class)); + } +} From f2924265291f07f25a883126d778eb8180886b85 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 17 Jun 2026 20:33:52 +0200 Subject: [PATCH 43/43] fix(wired): bound utility effect payloads --- .../wired/effects/WiredEffectAdjustClock.java | 19 +++--- .../WiredEffectChangeVariableValue.java | 4 +- .../effects/WiredEffectRemoveVariable.java | 4 +- .../wired/effects/WiredEffectResetTimers.java | 15 ++--- .../wired/effects/WiredEffectSendSignal.java | 6 +- .../wired/effects/WiredEffectSetAltitude.java | 17 ++--- .../wired/effects/WiredEffectWhisper.java | 21 +++--- .../effects/WiredUtilityPayloadGuard.java | 67 +++++++++++++++++++ .../effects/WiredUtilityPayloadGuardTest.java | 32 +++++++++ 9 files changed, 142 insertions(+), 43 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredUtilityPayloadGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredUtilityPayloadGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAdjustClock.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAdjustClock.java index 183eec7d..05f43b74 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAdjustClock.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectAdjustClock.java @@ -93,16 +93,17 @@ public class WiredEffectAdjustClock extends InteractionWiredEffect { this.items.clear(); String wiredData = set.getString("wired_data"); - if (wiredData != null && wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.setDelay(data.delay); - this.operator = this.normalizeOperator(data.operator); - this.furniSource = data.furniSource; - this.minutes = this.normalizeMinutes(data.minutes); - this.halfSecondSteps = this.normalizeHalfSecondSteps(data.halfSecondSteps); + JsonData jsonData = WiredUtilityPayloadGuard.fromJson(wiredData, JsonData.class); + if (jsonData != null) { + this.setDelay(WiredUtilityPayloadGuard.delay(jsonData.delay)); + this.operator = this.normalizeOperator(jsonData.operator); + this.furniSource = WiredUtilityPayloadGuard.furniSource(jsonData.furniSource); + this.minutes = this.normalizeMinutes(jsonData.minutes); + this.halfSecondSteps = this.normalizeHalfSecondSteps(jsonData.halfSecondSteps); - if (data.itemIds != null) { - for (Integer id : data.itemIds) { + if (jsonData.itemIds != null) { + for (Integer id : jsonData.itemIds) { + if (id == null) continue; HabboItem item = room.getHabboItem(id); if (item != null) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeVariableValue.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeVariableValue.java index 5e7e5c37..4d5b1b8f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeVariableValue.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectChangeVariableValue.java @@ -298,7 +298,7 @@ public class WiredEffectChangeVariableValue extends InteractionWiredEffect { String wiredData = set.getString("wired_data"); if (wiredData == null || wiredData.isEmpty() || !wiredData.startsWith("{")) return; - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + JsonData data = WiredUtilityPayloadGuard.fromJson(wiredData, JsonData.class); if (data == null) return; this.destinationTargetType = normalizeTargetType(data.destinationTargetType); @@ -312,7 +312,7 @@ public class WiredEffectChangeVariableValue extends InteractionWiredEffect { this.destinationFurniSource = normalizeDestinationFurniSource(data.destinationFurniSource); this.referenceUserSource = normalizeUserSource(data.referenceUserSource); this.referenceFurniSource = normalizeReferenceFurniSource(data.referenceFurniSource); - this.setDelay(Math.max(0, data.delay)); + this.setDelay(WiredUtilityPayloadGuard.delay(data.delay)); if (room != null) { try { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRemoveVariable.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRemoveVariable.java index d2812e61..2f574a73 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRemoveVariable.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectRemoveVariable.java @@ -260,14 +260,14 @@ public class WiredEffectRemoveVariable extends InteractionWiredEffect { } if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); + JsonData data = WiredUtilityPayloadGuard.fromJson(wiredData, JsonData.class); if (data != null) { this.variableItemId = Math.max(0, data.variableItemId); this.targetType = normalizeTargetType(data.targetType); this.userSource = normalizeUserSource(data.userSource); this.furniSource = normalizeFurniSource(data.furniSource); - this.setDelay(Math.max(0, data.delay)); + this.setDelay(WiredUtilityPayloadGuard.delay(data.delay)); if (room != null && data.selectedFurniIds != null) { for (Integer itemId : data.selectedFurniIds) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectResetTimers.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectResetTimers.java index e0abd1da..e4cb1b89 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectResetTimers.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectResetTimers.java @@ -96,19 +96,16 @@ public class WiredEffectResetTimers extends InteractionWiredEffect { public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); - if (wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.delay = data.delay; + JsonData jsonData = WiredUtilityPayloadGuard.fromJson(wiredData, JsonData.class); + if (jsonData != null) { + this.delay = WiredUtilityPayloadGuard.delay(jsonData.delay); } else { - try { - if (!wiredData.equals("")) { - this.delay = Integer.parseInt(wiredData); - } - } catch (Exception e) { + if (wiredData != null && !wiredData.equals("")) { + this.delay = WiredUtilityPayloadGuard.parseDelay(wiredData); } } - this.setDelay(this.delay); + this.setDelay(WiredUtilityPayloadGuard.delay(this.delay)); } @Override diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java index 99a14b67..27efef2f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSendSignal.java @@ -350,9 +350,9 @@ public class WiredEffectSendSignal extends InteractionWiredEffect { this.forwardItems = new THashSet<>(); String wiredData = set.getString("wired_data"); - if (wiredData != null && wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.setDelay(data.delay); + JsonData data = WiredUtilityPayloadGuard.fromJson(wiredData, JsonData.class); + if (data != null) { + this.setDelay(WiredUtilityPayloadGuard.delay(data.delay)); this.antennaSource = data.antennaSource; this.furniForward = normalizeSource(data.furniForward); this.userForward = normalizeSource(data.userForward); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSetAltitude.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSetAltitude.java index 9233daed..6675e54b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSetAltitude.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectSetAltitude.java @@ -100,15 +100,16 @@ public class WiredEffectSetAltitude extends InteractionWiredEffect { this.items.clear(); String wiredData = set.getString("wired_data"); - if (wiredData != null && wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.setDelay(data.delay); - this.operator = this.normalizeOperator(data.operator); - this.altitude = this.parseAltitudeOrDefault(data.altitude); - this.furniSource = data.furniSource; + JsonData jsonData = WiredUtilityPayloadGuard.fromJson(wiredData, JsonData.class); + if (jsonData != null) { + this.setDelay(WiredUtilityPayloadGuard.delay(jsonData.delay)); + this.operator = this.normalizeOperator(jsonData.operator); + this.altitude = this.parseAltitudeOrDefault(jsonData.altitude); + this.furniSource = WiredUtilityPayloadGuard.furniSource(jsonData.furniSource); - if (data.itemIds != null) { - for (Integer id : data.itemIds) { + if (jsonData.itemIds != null) { + for (Integer id : jsonData.itemIds) { + if (id == null) continue; HabboItem item = room.getHabboItem(id); if (item != null) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java index a2b858f4..25b7aa83 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredEffectWhisper.java @@ -272,22 +272,23 @@ public class WiredEffectWhisper extends InteractionWiredEffect { public void loadWiredData(ResultSet set, Room room) throws SQLException { String wiredData = set.getString("wired_data"); - if(wiredData.startsWith("{")) { - JsonData data = WiredManager.getGson().fromJson(wiredData, JsonData.class); - this.setDelay(data.delay); - this.message = data.message; - this.userSource = (data.userSource != null) ? data.userSource : WiredSourceUtil.SOURCE_TRIGGER; - this.visibilitySelection = (data.visibilitySelection != null && data.visibilitySelection == VISIBILITY_ALL_ROOM_USERS) + JsonData jsonData = WiredUtilityPayloadGuard.fromJson(wiredData, JsonData.class); + if(jsonData != null) { + this.setDelay(WiredUtilityPayloadGuard.delay(jsonData.delay)); + this.message = WiredUtilityPayloadGuard.text(jsonData.message); + this.userSource = (jsonData.userSource != null) ? WiredUtilityPayloadGuard.userSource(jsonData.userSource) : WiredSourceUtil.SOURCE_TRIGGER; + this.visibilitySelection = (jsonData.visibilitySelection != null && jsonData.visibilitySelection == VISIBILITY_ALL_ROOM_USERS) ? VISIBILITY_ALL_ROOM_USERS : VISIBILITY_SOURCE_USERS; - this.bubbleStyle = (data.bubbleStyle != null) ? data.bubbleStyle : RoomChatMessageBubbles.WIRED.getType(); + this.bubbleStyle = (jsonData.bubbleStyle != null) ? jsonData.bubbleStyle : RoomChatMessageBubbles.WIRED.getType(); } else { this.message = ""; - if (wiredData.split("\t").length >= 2) { - super.setDelay(Integer.parseInt(wiredData.split("\t")[0])); - this.message = wiredData.split("\t")[1]; + String[] wiredDataSplit = wiredData != null ? wiredData.split("\t") : new String[0]; + if (wiredDataSplit.length >= 2) { + super.setDelay(WiredUtilityPayloadGuard.parseDelay(wiredDataSplit[0])); + this.message = wiredDataSplit[1]; } this.userSource = WiredSourceUtil.SOURCE_TRIGGER; diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredUtilityPayloadGuard.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredUtilityPayloadGuard.java new file mode 100644 index 00000000..a74184ae --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredUtilityPayloadGuard.java @@ -0,0 +1,67 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.habbohotel.wired.core.WiredManager; +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; + +final class WiredUtilityPayloadGuard { + static final int MAX_LOAD_DELAY = 3600; + + private WiredUtilityPayloadGuard() { + } + + static int delay(int value) { + if (value < 0) { + return 0; + } + + return Math.min(value, MAX_LOAD_DELAY); + } + + static int parseDelay(String value) { + return delay(parseInt(value, 0)); + } + + static int parseInt(String value, int fallback) { + if (value == null) { + return fallback; + } + + try { + return Integer.parseInt(value.trim()); + } catch (RuntimeException e) { + return fallback; + } + } + + static int userSource(int value) { + return WiredSourceUtil.isDefaultUserSource(value) ? value : WiredSourceUtil.SOURCE_TRIGGER; + } + + static int furniSource(int value) { + switch (value) { + case WiredSourceUtil.SOURCE_TRIGGER: + case WiredSourceUtil.SOURCE_SELECTED: + case WiredSourceUtil.SOURCE_SELECTOR: + case WiredSourceUtil.SOURCE_SIGNAL: + return value; + default: + return WiredSourceUtil.SOURCE_TRIGGER; + } + } + + static String text(String value) { + return value == null ? "" : value; + } + + static T fromJson(String wiredData, Class type) { + if (wiredData == null || !wiredData.startsWith("{")) { + return null; + } + + try { + return WiredManager.getGson().fromJson(wiredData, type); + } catch (RuntimeException e) { + return null; + } + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredUtilityPayloadGuardTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredUtilityPayloadGuardTest.java new file mode 100644 index 00000000..716afb02 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/items/interactions/wired/effects/WiredUtilityPayloadGuardTest.java @@ -0,0 +1,32 @@ +package com.eu.habbo.habbohotel.items.interactions.wired.effects; + +import com.eu.habbo.habbohotel.wired.core.WiredSourceUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class WiredUtilityPayloadGuardTest { + @Test + void clampsDelayAndParsesLegacyNumbers() { + assertEquals(0, WiredUtilityPayloadGuard.delay(-1)); + assertEquals(20, WiredUtilityPayloadGuard.parseDelay("20")); + assertEquals(0, WiredUtilityPayloadGuard.parseDelay("bad")); + assertEquals(WiredUtilityPayloadGuard.MAX_LOAD_DELAY, WiredUtilityPayloadGuard.delay(Integer.MAX_VALUE)); + } + + @Test + void normalizesSourcesAndText() { + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, WiredUtilityPayloadGuard.userSource(999)); + assertEquals(WiredSourceUtil.SOURCE_SELECTOR, WiredUtilityPayloadGuard.userSource(WiredSourceUtil.SOURCE_SELECTOR)); + assertEquals(WiredSourceUtil.SOURCE_TRIGGER, WiredUtilityPayloadGuard.furniSource(-1)); + assertEquals(WiredSourceUtil.SOURCE_SELECTED, WiredUtilityPayloadGuard.furniSource(WiredSourceUtil.SOURCE_SELECTED)); + assertEquals("", WiredUtilityPayloadGuard.text(null)); + } + + @Test + void malformedJsonReturnsNull() { + assertNull(WiredUtilityPayloadGuard.fromJson("{broken", WiredEffectResetTimers.JsonData.class)); + assertNull(WiredUtilityPayloadGuard.fromJson(null, WiredEffectResetTimers.JsonData.class)); + } +}