From dba0337a7bceacbec434acb9c92cfc404475ddd2 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 14 Jun 2026 16:45:04 +0200 Subject: [PATCH] fix(rcon): validate grant requests --- .../eu/habbo/messages/rcon/GiveCredits.java | 12 +++++ .../eu/habbo/messages/rcon/GivePixels.java | 18 +++++++ .../eu/habbo/messages/rcon/GivePoints.java | 21 ++++++++ .../eu/habbo/messages/rcon/GiveRespect.java | 34 ++++++++++++- .../habbo/messages/rcon/RconGrantGuard.java | 49 +++++++++++++++++++ .../habbo/messages/rcon/RconUserLookup.java | 28 +++++++++++ .../rcon/GiveCreditsContractTest.java | 2 + .../messages/rcon/GivePixelsContractTest.java | 4 ++ .../messages/rcon/GivePointsContractTest.java | 28 +++++++++++ .../rcon/GiveRespectContractTest.java | 4 ++ .../messages/rcon/RconGrantGuardTest.java | 38 ++++++++++++++ 11 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/rcon/RconGrantGuard.java create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/rcon/RconUserLookup.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/rcon/GivePointsContractTest.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/rcon/RconGrantGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/rcon/GiveCredits.java b/Emulator/src/main/java/com/eu/habbo/messages/rcon/GiveCredits.java index d5f906ed..ed6c2f49 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/rcon/GiveCredits.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/rcon/GiveCredits.java @@ -21,6 +21,18 @@ public class GiveCredits extends RCONMessage { @Override public void handle(Gson gson, JSONGiveCredits object) { + int maxAmount = RconGrantGuard.parseMaxAmount( + Emulator.getConfig().getValue("rcon.grant.max_amount", String.valueOf(RconGrantGuard.DEFAULT_MAX_AMOUNT))); + String validationError = RconGrantGuard.validateUserId(object.user_id); + if (validationError == null) { + validationError = RconGrantGuard.validatePositiveAmount(object.credits, maxAmount, "credits"); + } + if (validationError != null) { + this.status = RCONMessage.STATUS_ERROR; + this.message = validationError; + return; + } + Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(object.user_id); if (habbo != null) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/rcon/GivePixels.java b/Emulator/src/main/java/com/eu/habbo/messages/rcon/GivePixels.java index 3e0f228d..137c3bd1 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/rcon/GivePixels.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/rcon/GivePixels.java @@ -21,11 +21,29 @@ public class GivePixels extends RCONMessage { @Override public void handle(Gson gson, JSONGivePixels object) { + int maxAmount = RconGrantGuard.parseMaxAmount( + Emulator.getConfig().getValue("rcon.grant.max_amount", String.valueOf(RconGrantGuard.DEFAULT_MAX_AMOUNT))); + String validationError = RconGrantGuard.validateUserId(object.user_id); + if (validationError == null) { + validationError = RconGrantGuard.validatePositiveAmount(object.pixels, maxAmount, "pixels"); + } + if (validationError != null) { + this.status = RCONMessage.STATUS_ERROR; + this.message = validationError; + return; + } + Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(object.user_id); if (habbo != null) { habbo.givePixels(object.pixels); } else { + if (!RconUserLookup.userExists(object.user_id)) { + this.status = RCONMessage.HABBO_NOT_FOUND; + this.message = "user not found"; + return; + } + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_currency (`user_id`, `type`, `amount`) VALUES (?, 0, ?) ON DUPLICATE KEY UPDATE amount = amount + ?")) { statement.setInt(1, object.user_id); statement.setInt(2, object.pixels); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/rcon/GivePoints.java b/Emulator/src/main/java/com/eu/habbo/messages/rcon/GivePoints.java index 1d28481d..c6e7a564 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/rcon/GivePoints.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/rcon/GivePoints.java @@ -22,11 +22,32 @@ public class GivePoints extends RCONMessage { @Override public void handle(Gson gson, JSONGivePoints object) { + int maxAmount = RconGrantGuard.parseMaxAmount( + Emulator.getConfig().getValue("rcon.grant.max_amount", String.valueOf(RconGrantGuard.DEFAULT_MAX_AMOUNT))); + String validationError = RconGrantGuard.validateUserId(object.user_id); + if (validationError == null) { + validationError = RconGrantGuard.validateCurrencyType(object.type); + } + if (validationError == null) { + validationError = RconGrantGuard.validatePositiveAmount(object.points, maxAmount, "points"); + } + if (validationError != null) { + this.status = RCONMessage.STATUS_ERROR; + this.message = validationError; + return; + } + Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(object.user_id); if (habbo != null) { habbo.givePoints(object.type, object.points); } else { + if (!RconUserLookup.userExists(object.user_id)) { + this.status = RCONMessage.HABBO_NOT_FOUND; + this.message = "user not found"; + return; + } + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_currency (`user_id`, `type`, `amount`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE amount = amount + ?")) { statement.setInt(1, object.user_id); statement.setInt(2, object.type); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/rcon/GiveRespect.java b/Emulator/src/main/java/com/eu/habbo/messages/rcon/GiveRespect.java index 3b9a57b2..1efb3a56 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/rcon/GiveRespect.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/rcon/GiveRespect.java @@ -21,6 +21,27 @@ public class GiveRespect extends RCONMessage { @Override public void handle(Gson gson, JSONGiveRespect object) { + int maxAmount = RconGrantGuard.parseMaxAmount( + Emulator.getConfig().getValue("rcon.grant.max_amount", String.valueOf(RconGrantGuard.DEFAULT_MAX_AMOUNT))); + String validationError = RconGrantGuard.validateUserId(object.user_id); + if (validationError == null) { + validationError = RconGrantGuard.validateNonNegativeAmount(object.respect_given, maxAmount, "respect_given"); + } + if (validationError == null) { + validationError = RconGrantGuard.validateNonNegativeAmount(object.respect_received, maxAmount, "respect_received"); + } + if (validationError == null) { + validationError = RconGrantGuard.validateNonNegativeAmount(object.daily_respects, maxAmount, "daily_respects"); + } + if (validationError == null && object.respect_given == 0 && object.respect_received == 0 && object.daily_respects == 0) { + validationError = "no respect grant provided"; + } + if (validationError != null) { + this.status = RCONMessage.STATUS_ERROR; + this.message = validationError; + return; + } + Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(object.user_id); if (habbo != null) { @@ -29,15 +50,26 @@ public class GiveRespect extends RCONMessage { habbo.getHabboStats().respectPointsToGive += object.daily_respects; habbo.getClient().sendResponse(new UserDataComposer(habbo)); } else { + if (!RconUserLookup.userExists(object.user_id)) { + this.status = RCONMessage.HABBO_NOT_FOUND; + this.message = "user not found"; + return; + } + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET respects_given = respects_given + ?, respects_received = respects_received + ?, daily_respect_points = daily_respect_points + ? WHERE user_id = ? LIMIT 1")) { statement.setInt(1, object.respect_given); statement.setInt(2, object.respect_received); statement.setInt(3, object.daily_respects); statement.setInt(4, object.user_id); - statement.execute(); + if (statement.executeUpdate() == 0) { + this.status = RCONMessage.HABBO_NOT_FOUND; + this.message = "user not found"; + return; + } } catch (SQLException e) { this.status = RCONMessage.SYSTEM_ERROR; LOGGER.error("Caught SQL exception", e); + return; } this.message = "offline"; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/rcon/RconGrantGuard.java b/Emulator/src/main/java/com/eu/habbo/messages/rcon/RconGrantGuard.java new file mode 100644 index 00000000..3b5864f6 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/rcon/RconGrantGuard.java @@ -0,0 +1,49 @@ +package com.eu.habbo.messages.rcon; + +public class RconGrantGuard { + public static final int DEFAULT_MAX_AMOUNT = 1_000_000; + + private RconGrantGuard() { + } + + public static String validateUserId(int userId) { + return userId > 0 ? null : "invalid user"; + } + + public static String validatePositiveAmount(int amount, int maxAmount, String fieldName) { + if (amount <= 0) { + return "invalid " + fieldName; + } + + if (maxAmount > 0 && amount > maxAmount) { + return fieldName + " exceeds rcon grant ceiling"; + } + + return null; + } + + public static String validateNonNegativeAmount(int amount, int maxAmount, String fieldName) { + if (amount < 0) { + return "invalid " + fieldName; + } + + if (maxAmount > 0 && amount > maxAmount) { + return fieldName + " exceeds rcon grant ceiling"; + } + + return null; + } + + public static String validateCurrencyType(int type) { + return type >= 0 ? null : "invalid currency type"; + } + + public static int parseMaxAmount(String rawValue) { + try { + int value = Integer.parseInt(rawValue); + return value > 0 ? value : DEFAULT_MAX_AMOUNT; + } catch (Exception e) { + return DEFAULT_MAX_AMOUNT; + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/rcon/RconUserLookup.java b/Emulator/src/main/java/com/eu/habbo/messages/rcon/RconUserLookup.java new file mode 100644 index 00000000..548cb517 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/rcon/RconUserLookup.java @@ -0,0 +1,28 @@ +package com.eu.habbo.messages.rcon; + +import com.eu.habbo.Emulator; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; + +public class RconUserLookup { + private RconUserLookup() { + } + + public static boolean userExists(int userId) { + 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; + } + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/rcon/GiveCreditsContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/rcon/GiveCreditsContractTest.java index dda7da84..24f7caba 100644 --- a/Emulator/src/test/java/com/eu/habbo/messages/rcon/GiveCreditsContractTest.java +++ b/Emulator/src/test/java/com/eu/habbo/messages/rcon/GiveCreditsContractTest.java @@ -20,5 +20,7 @@ class GiveCreditsContractTest { "Offline RCON credit grants must inspect the affected row count"); assertTrue(source.contains("HABBO_NOT_FOUND"), "Offline RCON credit grants must report missing users when the UPDATE changes no rows"); + assertTrue(source.contains("RconGrantGuard.validatePositiveAmount"), + "RCON credit grants must reject zero, negative, and oversized grants"); } } diff --git a/Emulator/src/test/java/com/eu/habbo/messages/rcon/GivePixelsContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/rcon/GivePixelsContractTest.java index af404d5c..fc06f148 100644 --- a/Emulator/src/test/java/com/eu/habbo/messages/rcon/GivePixelsContractTest.java +++ b/Emulator/src/test/java/com/eu/habbo/messages/rcon/GivePixelsContractTest.java @@ -20,5 +20,9 @@ class GivePixelsContractTest { "Offline RCON pixel grants must create the users_currency type 0 row when it is missing"); assertTrue(source.contains("ON DUPLICATE KEY UPDATE"), "Offline RCON pixel grants should increment existing rows with an upsert"); + assertTrue(source.contains("RconGrantGuard.validatePositiveAmount"), + "RCON pixel grants must reject zero, negative, and oversized grants"); + assertTrue(source.contains("RconUserLookup.userExists"), + "Offline RCON pixel grants must not create orphan currency rows for missing users"); } } diff --git a/Emulator/src/test/java/com/eu/habbo/messages/rcon/GivePointsContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/rcon/GivePointsContractTest.java new file mode 100644 index 00000000..8fd43fc6 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/rcon/GivePointsContractTest.java @@ -0,0 +1,28 @@ +package com.eu.habbo.messages.rcon; + +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 GivePointsContractTest { + private static String givePointsSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/GivePoints.java")); + } + + @Test + void pointGrantsValidateAmountTypeAndOfflineUserExistence() throws Exception { + String source = givePointsSource(); + + assertTrue(source.contains("RconGrantGuard.validateCurrencyType"), + "RCON point grants must reject invalid currency types"); + assertTrue(source.contains("RconGrantGuard.validatePositiveAmount"), + "RCON point grants must reject zero, negative, and oversized grants"); + assertTrue(source.contains("RconUserLookup.userExists"), + "Offline RCON point grants must not create orphan currency rows for missing users"); + assertTrue(source.contains("ON DUPLICATE KEY UPDATE"), + "Offline RCON point grants should increment existing rows with an upsert"); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/rcon/GiveRespectContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/rcon/GiveRespectContractTest.java index b36999c3..de541783 100644 --- a/Emulator/src/test/java/com/eu/habbo/messages/rcon/GiveRespectContractTest.java +++ b/Emulator/src/test/java/com/eu/habbo/messages/rcon/GiveRespectContractTest.java @@ -20,5 +20,9 @@ class GiveRespectContractTest { "respects_given must be incremented with respect_given"); assertTrue(source.contains("statement.setInt(2, object.respect_received);"), "respects_received must be incremented with respect_received"); + assertTrue(source.contains("RconGrantGuard.validateNonNegativeAmount"), + "RCON respect grants must reject negative values"); + assertTrue(source.contains("statement.executeUpdate() == 0"), + "Offline RCON respect grants must report missing users when the UPDATE changes no rows"); } } diff --git a/Emulator/src/test/java/com/eu/habbo/messages/rcon/RconGrantGuardTest.java b/Emulator/src/test/java/com/eu/habbo/messages/rcon/RconGrantGuardTest.java new file mode 100644 index 00000000..46ed1bdb --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/rcon/RconGrantGuardTest.java @@ -0,0 +1,38 @@ +package com.eu.habbo.messages.rcon; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class RconGrantGuardTest { + @Test + void validatesPositiveGrantAmounts() { + assertNull(RconGrantGuard.validatePositiveAmount(1, 100, "credits")); + assertEquals("invalid credits", RconGrantGuard.validatePositiveAmount(0, 100, "credits")); + assertEquals("invalid credits", RconGrantGuard.validatePositiveAmount(-1, 100, "credits")); + assertEquals("credits exceeds rcon grant ceiling", RconGrantGuard.validatePositiveAmount(101, 100, "credits")); + } + + @Test + void validatesNonNegativeGrantAmounts() { + assertNull(RconGrantGuard.validateNonNegativeAmount(0, 100, "respect_given")); + assertEquals("invalid respect_given", RconGrantGuard.validateNonNegativeAmount(-1, 100, "respect_given")); + assertEquals("respect_given exceeds rcon grant ceiling", RconGrantGuard.validateNonNegativeAmount(101, 100, "respect_given")); + } + + @Test + void validatesUserAndCurrencyIdentifiers() { + assertNull(RconGrantGuard.validateUserId(1)); + assertEquals("invalid user", RconGrantGuard.validateUserId(0)); + assertNull(RconGrantGuard.validateCurrencyType(0)); + assertEquals("invalid currency type", RconGrantGuard.validateCurrencyType(-1)); + } + + @Test + void parsesInvalidGrantCeilingsAsDefault() { + assertEquals(RconGrantGuard.DEFAULT_MAX_AMOUNT, RconGrantGuard.parseMaxAmount(null)); + assertEquals(RconGrantGuard.DEFAULT_MAX_AMOUNT, RconGrantGuard.parseMaxAmount("0")); + assertEquals(500, RconGrantGuard.parseMaxAmount("500")); + } +}