diff --git a/Emulator/src/main/java/com/eu/habbo/messages/rcon/UpdateUser.java b/Emulator/src/main/java/com/eu/habbo/messages/rcon/UpdateUser.java index 7357a760..14800d18 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/rcon/UpdateUser.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/rcon/UpdateUser.java @@ -12,9 +12,13 @@ import org.slf4j.LoggerFactory; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.util.regex.Pattern; public class UpdateUser extends RCONMessage { private static final Logger LOGGER = LoggerFactory.getLogger(UpdateUser.class); + static final int DEFAULT_MAX_ACHIEVEMENT_SCORE_DELTA = 10_000; + static final int MAX_LOOK_LENGTH = 256; + private static final Pattern LOOK_PATTERN = Pattern.compile("^[A-Za-z0-9.-]{1,256}$"); public UpdateUser() { super(UpdateUser.JSON.class); @@ -23,6 +27,19 @@ public class UpdateUser extends RCONMessage { @Override public void handle(Gson gson, JSON json) { if (json.user_id > 0) { + int maxAchievementScoreDelta = parseMaxAchievementScoreDelta(Emulator.getConfig().getValue("rcon.updateuser.max_achievement_score_delta", String.valueOf(DEFAULT_MAX_ACHIEVEMENT_SCORE_DELTA))); + if (!isValidAchievementScoreDelta(json.achievement_score, maxAchievementScoreDelta)) { + this.status = RCONMessage.STATUS_ERROR; + this.message = "invalid achievement score"; + return; + } + + if (!isValidLook(json.look)) { + this.status = RCONMessage.STATUS_ERROR; + this.message = "invalid look"; + return; + } + Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(json.user_id); if (habbo != null) { @@ -97,23 +114,56 @@ public class UpdateUser extends RCONMessage { index++; } statement.setInt(index, json.user_id); - statement.execute(); + if (statement.executeUpdate() == 0) { + this.status = RCONMessage.HABBO_NOT_FOUND; + this.message = "user not found"; + return; + } } if (!json.look.isEmpty()) { try (PreparedStatement statement = connection.prepareStatement("UPDATE users SET look = ? WHERE id = ? LIMIT 1")) { statement.setString(1, json.look); statement.setInt(2, json.user_id); - statement.execute(); + if (statement.executeUpdate() == 0) { + this.status = RCONMessage.HABBO_NOT_FOUND; + this.message = "user not found"; + return; + } } } } catch (SQLException e) { LOGGER.error("Caught SQL exception", e); + this.status = RCONMessage.SYSTEM_ERROR; + this.message = "failed to update user"; } } + } else { + this.status = RCONMessage.STATUS_ERROR; + this.message = "invalid user"; } } + static boolean isValidAchievementScoreDelta(int achievementScoreDelta, int maxAchievementScoreDelta) { + return achievementScoreDelta >= 0 && achievementScoreDelta <= maxAchievementScoreDelta; + } + + static int parseMaxAchievementScoreDelta(String configured) { + try { + int parsed = Integer.parseInt(configured); + if (parsed >= 0) { + return parsed; + } + } catch (NumberFormatException ignored) { + } + + return DEFAULT_MAX_ACHIEVEMENT_SCORE_DELTA; + } + + static boolean isValidLook(String look) { + return look == null || look.isEmpty() || (look.length() <= MAX_LOOK_LENGTH && LOOK_PATTERN.matcher(look).matches()); + } + static class JSON { public int user_id; @@ -142,4 +192,4 @@ public class UpdateUser extends RCONMessage { public boolean strip_unredeemed_clothing = false; //More could be added in the future. } -} \ No newline at end of file +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/rcon/UpdateUserGuardTest.java b/Emulator/src/test/java/com/eu/habbo/messages/rcon/UpdateUserGuardTest.java new file mode 100644 index 00000000..0ca4dc2a --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/rcon/UpdateUserGuardTest.java @@ -0,0 +1,50 @@ +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.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class UpdateUserGuardTest { + @Test + void validatesAchievementScoreDelta() { + assertTrue(UpdateUser.isValidAchievementScoreDelta(0, 100)); + assertTrue(UpdateUser.isValidAchievementScoreDelta(100, 100)); + assertFalse(UpdateUser.isValidAchievementScoreDelta(-1, 100)); + assertFalse(UpdateUser.isValidAchievementScoreDelta(101, 100)); + } + + @Test + void parsesInvalidAchievementScoreCeilingsAsDefault() { + assertEquals(UpdateUser.DEFAULT_MAX_ACHIEVEMENT_SCORE_DELTA, UpdateUser.parseMaxAchievementScoreDelta(null)); + assertEquals(UpdateUser.DEFAULT_MAX_ACHIEVEMENT_SCORE_DELTA, UpdateUser.parseMaxAchievementScoreDelta("-1")); + assertEquals(0, UpdateUser.parseMaxAchievementScoreDelta("0")); + assertEquals(50, UpdateUser.parseMaxAchievementScoreDelta("50")); + } + + @Test + void validatesLookShapeAndLength() { + assertTrue(UpdateUser.isValidLook(null)); + assertTrue(UpdateUser.isValidLook("")); + assertTrue(UpdateUser.isValidLook("hr-115-42.hd-195-19.ch-3030-82")); + assertFalse(UpdateUser.isValidLook("hd-1\nch-1")); + assertFalse(UpdateUser.isValidLook("hd_1")); + assertFalse(UpdateUser.isValidLook("a".repeat(UpdateUser.MAX_LOOK_LENGTH + 1))); + } + + @Test + void offlineUpdatesReportMissingUsersAndUseAffectedRows() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/UpdateUser.java")); + + assertTrue(source.contains("executeUpdate() == 0"), + "Offline UpdateUser mutations must inspect affected row counts"); + assertTrue(source.contains("HABBO_NOT_FOUND"), + "Offline UpdateUser mutations must report missing users"); + assertTrue(source.contains("rcon.updateuser.max_achievement_score_delta"), + "Achievement score deltas must have a configurable RCON ceiling"); + } +}