fix(catalog): bound marketplace inputs

This commit is contained in:
simoleo89
2026-06-16 21:30:37 +02:00
parent 416d0bb088
commit efe7897fb4
9 changed files with 161 additions and 8 deletions
@@ -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);
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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));
}
}
@@ -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();
@@ -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);
@@ -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);
}
}
@@ -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");
}
}
@@ -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));
}
}