From efe7897fb4da3dbb316b39c71dc4efc23e40c688 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 21:30:37 +0200 Subject: [PATCH] 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)); + } +}