From 15b56f951944c0905d8bd1d46e4aded277038e06 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 14 Jun 2026 21:13:24 +0200 Subject: [PATCH] fix(rcon): bound mute and achievement mutations --- .../src/main/java/com/eu/habbo/Emulator.java | 2 ++ .../com/eu/habbo/messages/rcon/MuteUser.java | 27 +++++++++++++-- .../messages/rcon/ProgressAchievement.java | 28 +++++++++++++-- .../messages/rcon/MuteUserGuardTest.java | 31 +++++++++++++++++ .../rcon/ProgressAchievementGuardTest.java | 34 +++++++++++++++++++ 5 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/rcon/MuteUserGuardTest.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/rcon/ProgressAchievementGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/Emulator.java b/Emulator/src/main/java/com/eu/habbo/Emulator.java index 86ceca74..63abc5f5 100644 --- a/Emulator/src/main/java/com/eu/habbo/Emulator.java +++ b/Emulator/src/main/java/com/eu/habbo/Emulator.java @@ -157,6 +157,8 @@ public final class Emulator { Emulator.config.register("rcon.rate_limit.limit_for_period", "60"); Emulator.config.register("rcon.rate_limit.refresh_period_ms", "1000"); Emulator.config.register("rcon.rate_limit.timeout_ms", "0"); + Emulator.config.register("rcon.mute.max_duration_seconds", "604800"); + Emulator.config.register("rcon.achievement.max_progress", "10000"); String hotelTimezoneId = Emulator.getConfig().getValue("hotel.timezone", java.time.ZoneId.systemDefault().getId()); System.out.println(); LOGGER.info("https://github.com/duckietm/Arcturus-Morningstar-Extended, "); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/rcon/MuteUser.java b/Emulator/src/main/java/com/eu/habbo/messages/rcon/MuteUser.java index 711b7d22..304172d4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/rcon/MuteUser.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/rcon/MuteUser.java @@ -3,6 +3,8 @@ package com.eu.habbo.messages.rcon; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.users.Habbo; import com.google.gson.Gson; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Positive; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,6 +14,7 @@ import java.sql.SQLException; public class MuteUser extends RCONMessage { private static final Logger LOGGER = LoggerFactory.getLogger(MuteUser.class); + static final int DEFAULT_MAX_DURATION_SECONDS = 604_800; public MuteUser() { super(MuteUser.JSON.class); @@ -19,6 +22,13 @@ public class MuteUser extends RCONMessage { @Override public void handle(Gson gson, JSON json) { + int maxDuration = parseMaxDuration(Emulator.getConfig().getValue("rcon.mute.max_duration_seconds", String.valueOf(DEFAULT_MAX_DURATION_SECONDS))); + if (json.duration < 0 || json.duration > maxDuration) { + this.status = RCONMessage.STATUS_ERROR; + this.message = "duration must be between 0 and " + maxDuration + " seconds"; + return; + } + Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(json.user_id); if (habbo != null) { @@ -29,7 +39,7 @@ public class MuteUser extends RCONMessage { } } else { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET mute_end_timestamp = ? WHERE user_id = ? LIMIT 1")) { - statement.setInt(1, Emulator.getIntUnixTimestamp() + json.duration); + statement.setInt(1, json.duration == 0 ? 0 : Emulator.getIntUnixTimestamp() + json.duration); statement.setInt(2, json.user_id); if (statement.executeUpdate() == 0) { this.status = HABBO_NOT_FOUND; @@ -40,11 +50,24 @@ public class MuteUser extends RCONMessage { } } + static int parseMaxDuration(String configured) { + try { + int parsed = Integer.parseInt(configured); + if (parsed >= 0) { + return parsed; + } + } catch (NumberFormatException ignored) { + } + + return DEFAULT_MAX_DURATION_SECONDS; + } + static class JSON { + @Positive(message = "invalid user") public int user_id; - + @Min(value = 0, message = "invalid duration") public int duration; } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/rcon/ProgressAchievement.java b/Emulator/src/main/java/com/eu/habbo/messages/rcon/ProgressAchievement.java index 69cc6d54..77160c8a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/rcon/ProgressAchievement.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/rcon/ProgressAchievement.java @@ -5,8 +5,10 @@ import com.eu.habbo.habbohotel.achievements.Achievement; import com.eu.habbo.habbohotel.achievements.AchievementManager; import com.eu.habbo.habbohotel.users.Habbo; import com.google.gson.Gson; +import jakarta.validation.constraints.Positive; public class ProgressAchievement extends RCONMessage { + static final int DEFAULT_MAX_PROGRESS = 10_000; public ProgressAchievement() { super(ProgressAchievementJSON.class); @@ -14,6 +16,13 @@ public class ProgressAchievement extends RCONMessage maxProgress) { + this.status = RCONMessage.STATUS_ERROR; + this.message = "progress must be between 1 and " + maxProgress; + return; + } + Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(json.user_id); if (habbo != null) { @@ -28,14 +37,27 @@ public class ProgressAchievement extends RCONMessage 0) { + return parsed; + } + } catch (NumberFormatException ignored) { + } + + return DEFAULT_MAX_PROGRESS; + } + static class ProgressAchievementJSON { + @Positive(message = "invalid user") public int user_id; - + @Positive(message = "invalid achievement") public int achievement_id; - + @Positive(message = "invalid progress") public int progress; } -} \ No newline at end of file +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/rcon/MuteUserGuardTest.java b/Emulator/src/test/java/com/eu/habbo/messages/rcon/MuteUserGuardTest.java new file mode 100644 index 00000000..11cdb9bc --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/rcon/MuteUserGuardTest.java @@ -0,0 +1,31 @@ +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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MuteUserGuardTest { + @Test + void parsesInvalidDurationCeilingsAsDefault() { + assertEquals(MuteUser.DEFAULT_MAX_DURATION_SECONDS, MuteUser.parseMaxDuration(null)); + assertEquals(MuteUser.DEFAULT_MAX_DURATION_SECONDS, MuteUser.parseMaxDuration("-1")); + assertEquals(0, MuteUser.parseMaxDuration("0")); + assertEquals(60, MuteUser.parseMaxDuration("60")); + } + + @Test + void rejectsNegativeAndOversizedMuteDurations() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/MuteUser.java")); + + assertTrue(source.contains("json.duration < 0 || json.duration > maxDuration"), + "RCON mute must reject negative durations and configured-duration overflows"); + assertTrue(source.contains("json.duration == 0 ? 0 : Emulator.getIntUnixTimestamp() + json.duration"), + "Offline unmute must clear mute_end_timestamp instead of writing the current timestamp"); + assertTrue(source.contains("rcon.mute.max_duration_seconds"), + "RCON mute duration ceiling must be configurable"); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/rcon/ProgressAchievementGuardTest.java b/Emulator/src/test/java/com/eu/habbo/messages/rcon/ProgressAchievementGuardTest.java new file mode 100644 index 00000000..6d26b7e8 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/rcon/ProgressAchievementGuardTest.java @@ -0,0 +1,34 @@ +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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ProgressAchievementGuardTest { + @Test + void parsesInvalidProgressCeilingsAsDefault() { + assertEquals(ProgressAchievement.DEFAULT_MAX_PROGRESS, ProgressAchievement.parseMaxProgress(null)); + assertEquals(ProgressAchievement.DEFAULT_MAX_PROGRESS, ProgressAchievement.parseMaxProgress("0")); + assertEquals(50, ProgressAchievement.parseMaxProgress("50")); + } + + @Test + void validatesAchievementProgressPayload() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/ProgressAchievement.java")); + + assertTrue(source.contains("@Positive(message = \"invalid user\")"), + "RCON achievement progress must reject invalid target users before execution"); + assertTrue(source.contains("@Positive(message = \"invalid achievement\")"), + "RCON achievement progress must reject invalid achievement ids before execution"); + assertTrue(source.contains("@Positive(message = \"invalid progress\")"), + "RCON achievement progress must reject zero or negative progress before execution"); + assertTrue(source.contains("json.progress > maxProgress"), + "RCON achievement progress must reject configured-progress overflows"); + assertTrue(source.contains("rcon.achievement.max_progress"), + "RCON achievement progress ceiling must be configurable"); + } +}