From fe0ba3b9e9eb185b821579b40b9967de917a6a18 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 14 Jun 2026 17:12:58 +0200 Subject: [PATCH] fix(housekeeping): validate grant mutations --- .../HousekeepingGiveCreditsEvent.java | 3 +- .../HousekeepingGiveCurrencyEvent.java | 8 ++- .../HousekeepingGrantItemEvent.java | 10 ++++ .../HousekeepingMutationGuard.java | 46 ++++++++++++++++ ...HousekeepingGrantMutationContractTest.java | 53 +++++++++++++++++++ .../HousekeepingMutationGuardTest.java | 24 +++++++++ 6 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMutationGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantMutationContractTest.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMutationGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java index 9b949df7..1cc102ad 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java @@ -12,7 +12,6 @@ import java.sql.SQLException; public class HousekeepingGiveCreditsEvent extends MessageHandler { private static final String ACTION_KEY = "user.give_credits"; - private static final int MAX_GRANT = 1_000_000_000; @Override public int getRatelimit() { @@ -28,7 +27,7 @@ public class HousekeepingGiveCreditsEvent extends MessageHandler { int userId = this.packet.readInt(); int amount = this.packet.readInt(); - if (userId <= 0 || amount == 0 || amount < -MAX_GRANT || amount > MAX_GRANT) { + if (userId <= 0 || !HousekeepingMutationGuard.isPositiveGrantAmount(amount)) { this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java index f141c4ce..f31e1934 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java @@ -18,7 +18,6 @@ import java.sql.SQLException; */ public class HousekeepingGiveCurrencyEvent extends MessageHandler { private static final int CURRENCY_DUCKETS = 0; - private static final int MAX_GRANT = 1_000_000_000; @Override public int getRatelimit() { @@ -37,7 +36,7 @@ public class HousekeepingGiveCurrencyEvent extends MessageHandler { String actionKey = "user.give_currency_" + currencyType; - if (userId <= 0 || amount == 0 || amount < -MAX_GRANT || amount > MAX_GRANT) { + if (userId <= 0 || !HousekeepingMutationGuard.isCurrencyType(currencyType) || !HousekeepingMutationGuard.isPositiveGrantAmount(amount)) { this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.invalid_input")); return; } @@ -47,6 +46,11 @@ public class HousekeepingGiveCurrencyEvent extends MessageHandler { return; } + if (!HousekeepingMutationGuard.userExists(userId)) { + this.client.sendResponse(new HousekeepingActionResultComposer(actionKey, false, 0, "housekeeping.error.user_not_found")); + return; + } + Habbo online = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); if (online != null) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java index 77da4407..6d44acf4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java @@ -45,6 +45,16 @@ public class HousekeepingGrantItemEvent extends MessageHandler { return; } + if (!HousekeepingMutationGuard.userExists(userId)) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found")); + return; + } + + if (!HousekeepingMutationGuard.itemExists(itemId)) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.item_not_found")); + return; + } + if (quantity > MAX_QUANTITY_PER_CALL) { quantity = MAX_QUANTITY_PER_CALL; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMutationGuard.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMutationGuard.java new file mode 100644 index 00000000..553f7f83 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMutationGuard.java @@ -0,0 +1,46 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import com.eu.habbo.Emulator; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +final class HousekeepingMutationGuard { + static final int MAX_GRANT = 1_000_000_000; + + private HousekeepingMutationGuard() { + } + + static boolean isPositiveGrantAmount(int amount) { + return amount > 0 && amount <= MAX_GRANT; + } + + static boolean isCurrencyType(int currencyType) { + return currencyType >= 0; + } + + static boolean userExists(int userId) { + if (userId <= 0) { + return false; + } + + if (Emulator.getGameEnvironment().getHabboManager().getHabbo(userId) != null) { + return true; + } + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE id = ? LIMIT 1")) { + statement.setInt(1, userId); + try (ResultSet set = statement.executeQuery()) { + return set.next(); + } + } catch (Exception e) { + return false; + } + } + + static boolean itemExists(int itemId) { + return itemId > 0 && Emulator.getGameEnvironment().getItemManager().getItem(itemId) != null; + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantMutationContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantMutationContractTest.java new file mode 100644 index 00000000..3199513b --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantMutationContractTest.java @@ -0,0 +1,53 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class HousekeepingGrantMutationContractTest { + private static final Path CREDITS_SOURCE = Path.of( + "src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCreditsEvent.java"); + private static final Path CURRENCY_SOURCE = Path.of( + "src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGiveCurrencyEvent.java"); + private static final Path GRANT_ITEM_SOURCE = Path.of( + "src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingGrantItemEvent.java"); + + @Test + void housekeepingGrantsRejectNegativeOrOversizedAmountsServerSide() throws IOException { + String credits = Files.readString(CREDITS_SOURCE); + String currency = Files.readString(CURRENCY_SOURCE); + + assertTrue(credits.contains("HousekeepingMutationGuard.isPositiveGrantAmount(amount)"), + "credit grants must only accept positive bounded amounts"); + assertTrue(currency.contains("HousekeepingMutationGuard.isPositiveGrantAmount(amount)"), + "currency grants must only accept positive bounded amounts"); + } + + @Test + void housekeepingCurrencyGrantsRejectInvalidTypesAndMissingUsers() throws IOException { + String currency = Files.readString(CURRENCY_SOURCE); + + assertTrue(currency.contains("HousekeepingMutationGuard.isCurrencyType(currencyType)"), + "currency grants must reject negative currency types"); + assertTrue(currency.contains("HousekeepingMutationGuard.userExists(userId)"), + "offline currency grants must not create orphan users_currency rows"); + } + + @Test + void housekeepingItemGrantsRequireRealUsersAndItemsBeforeInsert() throws IOException { + String grantItem = Files.readString(GRANT_ITEM_SOURCE); + + int userCheck = grantItem.indexOf("HousekeepingMutationGuard.userExists(userId)"); + int itemCheck = grantItem.indexOf("HousekeepingMutationGuard.itemExists(itemId)"); + int insert = grantItem.indexOf("INSERT INTO items"); + + assertTrue(userCheck >= 0, "item grants must check the target user exists"); + assertTrue(itemCheck >= 0, "item grants must check the item base exists"); + assertTrue(userCheck < insert, "target user must be validated before item insert"); + assertTrue(itemCheck < insert, "item base must be validated before item insert"); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMutationGuardTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMutationGuardTest.java new file mode 100644 index 00000000..72608f01 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMutationGuardTest.java @@ -0,0 +1,24 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class HousekeepingMutationGuardTest { + @Test + void positiveGrantAmountsMustBeStrictlyPositiveAndBounded() { + assertFalse(HousekeepingMutationGuard.isPositiveGrantAmount(-1)); + assertFalse(HousekeepingMutationGuard.isPositiveGrantAmount(0)); + assertTrue(HousekeepingMutationGuard.isPositiveGrantAmount(1)); + assertTrue(HousekeepingMutationGuard.isPositiveGrantAmount(HousekeepingMutationGuard.MAX_GRANT)); + assertFalse(HousekeepingMutationGuard.isPositiveGrantAmount(HousekeepingMutationGuard.MAX_GRANT + 1)); + } + + @Test + void currencyTypesCannotBeNegative() { + assertFalse(HousekeepingMutationGuard.isCurrencyType(-1)); + assertTrue(HousekeepingMutationGuard.isCurrencyType(0)); + assertTrue(HousekeepingMutationGuard.isCurrencyType(101)); + } +}