fix(rcon): bound mute and achievement mutations

This commit is contained in:
simoleo89
2026-06-14 21:13:24 +02:00
parent 8412a51ec4
commit 15b56f9519
5 changed files with 117 additions and 5 deletions
@@ -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, ");
@@ -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<MuteUser.JSON> {
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<MuteUser.JSON> {
@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<MuteUser.JSON> {
}
} 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<MuteUser.JSON> {
}
}
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;
}
}
@@ -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<ProgressAchievement.ProgressAchievementJSON> {
static final int DEFAULT_MAX_PROGRESS = 10_000;
public ProgressAchievement() {
super(ProgressAchievementJSON.class);
@@ -14,6 +16,13 @@ public class ProgressAchievement extends RCONMessage<ProgressAchievement.Progres
@Override
public void handle(Gson gson, ProgressAchievementJSON json) {
int maxProgress = parseMaxProgress(Emulator.getConfig().getValue("rcon.achievement.max_progress", String.valueOf(DEFAULT_MAX_PROGRESS)));
if (json.progress > 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<ProgressAchievement.Progres
}
}
static int parseMaxProgress(String configured) {
try {
int parsed = Integer.parseInt(configured);
if (parsed > 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;
}
}
}
@@ -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");
}
}
@@ -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");
}
}