From d9cf70910fcdb76baf8d9996666703addfcf39fd Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 14 Jun 2026 17:21:37 +0200 Subject: [PATCH] fix(housekeeping): cap sanction durations safely --- .../HousekeepingBanUserEvent.java | 6 +-- .../HousekeepingMuteUserEvent.java | 3 +- .../HousekeepingSanctionDuration.java | 37 +++++++++++++++++ .../HousekeepingTradeLockUserEvent.java | 7 +--- ...sekeepingSanctionDurationContractTest.java | 40 +++++++++++++++++++ .../HousekeepingSanctionDurationTest.java | 21 ++++++++++ 6 files changed, 102 insertions(+), 12 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSanctionDuration.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSanctionDurationContractTest.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSanctionDurationTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java index 9d8faf2f..4d6e49f4 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java @@ -17,9 +17,6 @@ import java.util.List; */ public class HousekeepingBanUserEvent extends MessageHandler { private static final String ACTION_KEY = "user.ban"; - private static final int SECONDS_IN_HOUR = 3600; - // 100-year ceiling, matches ModToolSanctionBanEvent's permanent ban. - private static final int MAX_DURATION_SECONDS = 100 * 365 * 24 * 3600; @Override public int getRatelimit() { @@ -46,8 +43,7 @@ public class HousekeepingBanUserEvent extends MessageHandler { return; } - long durationLong = (long) hours * SECONDS_IN_HOUR; - int duration = durationLong > MAX_DURATION_SECONDS ? MAX_DURATION_SECONDS : (int) durationLong; + int duration = HousekeepingSanctionDuration.secondsFromHours(hours); List bans = Emulator.getGameEnvironment().getModToolManager() .ban(userId, this.client.getHabbo(), reason != null ? reason : "", duration, ModToolBanType.ACCOUNT, 0); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java index 68f4d346..ac019305 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java @@ -15,7 +15,6 @@ import com.eu.habbo.messages.outgoing.housekeeping.HousekeepingActionResultCompo */ public class HousekeepingMuteUserEvent extends MessageHandler { private static final String ACTION_KEY = "user.mute"; - private static final int SECONDS_IN_MINUTE = 60; @Override public int getRatelimit() { @@ -49,7 +48,7 @@ public class HousekeepingMuteUserEvent extends MessageHandler { return; } - target.mute(minutes * SECONDS_IN_MINUTE, false); + target.mute(HousekeepingSanctionDuration.secondsFromMinutes(minutes), false); if (reason != null && !reason.isEmpty()) { target.alert(reason); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSanctionDuration.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSanctionDuration.java new file mode 100644 index 00000000..8585d9c4 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSanctionDuration.java @@ -0,0 +1,37 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +final class HousekeepingSanctionDuration { + static final int SECONDS_IN_MINUTE = 60; + static final int SECONDS_IN_HOUR = 3600; + static final int MAX_SECONDS = Integer.MAX_VALUE; + + private HousekeepingSanctionDuration() { + } + + static int secondsFromHours(int hours) { + if (hours <= 0) { + return 0; + } + + long seconds = (long) hours * SECONDS_IN_HOUR; + return seconds > MAX_SECONDS ? MAX_SECONDS : (int) seconds; + } + + static int secondsFromMinutes(int minutes) { + if (minutes <= 0) { + return 0; + } + + long seconds = (long) minutes * SECONDS_IN_MINUTE; + return seconds > MAX_SECONDS ? MAX_SECONDS : (int) seconds; + } + + static int unixUntil(int now, int durationSeconds) { + if (durationSeconds <= 0) { + return now; + } + + long until = (long) now + durationSeconds; + return until > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) until; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java index 5547da07..0b284115 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java @@ -20,8 +20,6 @@ import java.sql.SQLException; */ public class HousekeepingTradeLockUserEvent extends MessageHandler { private static final String ACTION_KEY = "user.trade_lock"; - private static final int SECONDS_IN_HOUR = 3600; - private static final int MAX_DURATION_SECONDS = 100 * 365 * 24 * 3600; @Override public int getRatelimit() { @@ -48,9 +46,8 @@ public class HousekeepingTradeLockUserEvent extends MessageHandler { return; } - long durationLong = (long) hours * SECONDS_IN_HOUR; - int duration = durationLong > MAX_DURATION_SECONDS ? MAX_DURATION_SECONDS : (int) durationLong; - int lockedUntil = Emulator.getIntUnixTimestamp() + duration; + int duration = HousekeepingSanctionDuration.secondsFromHours(hours); + int lockedUntil = HousekeepingSanctionDuration.unixUntil(Emulator.getIntUnixTimestamp(), duration); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET trade_locked_until = ? WHERE user_id = ? LIMIT 1")) { diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSanctionDurationContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSanctionDurationContractTest.java new file mode 100644 index 00000000..3fb49a17 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSanctionDurationContractTest.java @@ -0,0 +1,40 @@ +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.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class HousekeepingSanctionDurationContractTest { + private static final Path BAN_SOURCE = Path.of( + "src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java"); + private static final Path MUTE_SOURCE = Path.of( + "src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java"); + private static final Path TRADE_LOCK_SOURCE = Path.of( + "src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java"); + + @Test + void sanctionsUseSharedOverflowSafeDurationHelpers() throws IOException { + String ban = Files.readString(BAN_SOURCE); + String mute = Files.readString(MUTE_SOURCE); + String tradeLock = Files.readString(TRADE_LOCK_SOURCE); + + assertTrue(ban.contains("HousekeepingSanctionDuration.secondsFromHours(hours)")); + assertTrue(mute.contains("HousekeepingSanctionDuration.secondsFromMinutes(minutes)")); + assertTrue(tradeLock.contains("HousekeepingSanctionDuration.secondsFromHours(hours)")); + assertTrue(tradeLock.contains("HousekeepingSanctionDuration.unixUntil(")); + } + + @Test + void sanctionsDoNotUseOverflowProneIntDurationConstants() throws IOException { + String ban = Files.readString(BAN_SOURCE); + String tradeLock = Files.readString(TRADE_LOCK_SOURCE); + + assertFalse(ban.contains("100 * 365 * 24 * 3600")); + assertFalse(tradeLock.contains("100 * 365 * 24 * 3600")); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSanctionDurationTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSanctionDurationTest.java new file mode 100644 index 00000000..f412f350 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSanctionDurationTest.java @@ -0,0 +1,21 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class HousekeepingSanctionDurationTest { + @Test + void convertsHoursAndMinutesWithoutIntegerOverflow() { + assertEquals(3600, HousekeepingSanctionDuration.secondsFromHours(1)); + assertEquals(60, HousekeepingSanctionDuration.secondsFromMinutes(1)); + assertEquals(Integer.MAX_VALUE, HousekeepingSanctionDuration.secondsFromHours(Integer.MAX_VALUE)); + assertEquals(Integer.MAX_VALUE, HousekeepingSanctionDuration.secondsFromMinutes(Integer.MAX_VALUE)); + } + + @Test + void capsUnixTimestampInsteadOfWrapping() { + assertEquals(1_000_060, HousekeepingSanctionDuration.unixUntil(1_000_000, 60)); + assertEquals(Integer.MAX_VALUE, HousekeepingSanctionDuration.unixUntil(Integer.MAX_VALUE - 10, 60)); + } +}