fix(housekeeping): cap sanction durations safely

This commit is contained in:
simoleo89
2026-06-14 17:21:37 +02:00
parent fe0ba3b9e9
commit d9cf70910f
6 changed files with 102 additions and 12 deletions
@@ -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<ModToolBan> bans = Emulator.getGameEnvironment().getModToolManager()
.ban(userId, this.client.getHabbo(), reason != null ? reason : "", duration, ModToolBanType.ACCOUNT, 0);
@@ -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);
@@ -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;
}
}
@@ -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")) {
@@ -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"));
}
}
@@ -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));
}
}