From 36a06647f0dc3e3fe7f920a90594b617ea59c49e Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 15 Jun 2026 19:51:36 +0200 Subject: [PATCH 1/4] fix(modtool): enforce staff target rank ceilings --- .../habbohotel/modtool/ModToolManager.java | 40 ++++++++++++++----- .../habbohotel/modtool/ModToolSanctions.java | 4 ++ .../ModToolIssueDefaultSanctionEvent.java | 4 ++ .../ModToolPermissionContractTest.java | 28 ++++++++++++- 4 files changed, 65 insertions(+), 11 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java index 9883ec7a..efa38b8b 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java @@ -3,6 +3,7 @@ package com.eu.habbo.habbohotel.modtool; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.gameclients.GameClient; import com.eu.habbo.habbohotel.permissions.Permission; +import com.eu.habbo.habbohotel.permissions.Rank; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.habbohotel.rooms.RoomState; import com.eu.habbo.habbohotel.users.Habbo; @@ -423,12 +424,16 @@ public class ModToolManager { } public void kick(Habbo moderator, Habbo target, String message) { - if (moderator.hasPermission(Permission.ACC_SUPPORTTOOL) && !target.hasPermission(Permission.ACC_UNKICKABLE)) { - if (target.getHabboInfo().getCurrentRoom() != null) { - Emulator.getGameEnvironment().getRoomManager().leaveRoom(target, target.getHabboInfo().getCurrentRoom()); - } - this.alert(moderator, target, message, SupportUserAlertedReason.KICKED); + if (moderator == null || target == null || !moderator.hasPermission(Permission.ACC_SUPPORTTOOL) || + target.hasPermission(Permission.ACC_UNKICKABLE) || + !canModerateTarget(moderator, target.getHabboInfo().getId())) { + return; } + + if (target.getHabboInfo().getCurrentRoom() != null) { + Emulator.getGameEnvironment().getRoomManager().leaveRoom(target, target.getHabboInfo().getCurrentRoom()); + } + this.alert(moderator, target, message, SupportUserAlertedReason.KICKED); } public List ban(int targetUserId, Habbo moderator, String reason, int duration, ModToolBanType type, int cfhTopic) { @@ -443,7 +448,7 @@ public class ModToolManager { return bans; } - if (moderator.getHabboInfo().getRank().getId() < offlineInfo.getRank().getId()) { + if (!canModerateTarget(moderator, targetUserId)) { return bans; } @@ -468,7 +473,7 @@ public class ModToolManager { if ((type == ModToolBanType.IP || type == ModToolBanType.SUPER) && target != null && !ban.ip.equals("offline")) { for (Habbo h : Emulator.getGameServer().getGameClientManager().getHabbosWithIP(ban.ip)) { - if (h.getHabboInfo().getRank().getId() >= moderator.getHabboInfo().getRank().getId()) continue; + if (!canModerateTarget(moderator, h.getHabboInfo().getId())) continue; ban = new ModToolBan(h.getHabboInfo().getId(), h != null ? h.getHabboInfo().getIpLogin() : "offline", h != null ? h.getClient().getMachineId() : "offline", moderator.getHabboInfo().getId(), Emulator.getIntUnixTimestamp() + duration, reason, type, cfhTopic); Emulator.getPluginManager().fireEvent(new SupportUserBannedEvent(moderator, h, ban)); @@ -480,7 +485,7 @@ public class ModToolManager { if ((type == ModToolBanType.MACHINE || type == ModToolBanType.SUPER) && target != null && !ban.machineId.equals("offline")) { for (Habbo h : Emulator.getGameServer().getGameClientManager().getHabbosWithMachineId(ban.machineId)) { - if (h.getHabboInfo().getRank().getId() >= moderator.getHabboInfo().getRank().getId()) continue; + if (!canModerateTarget(moderator, h.getHabboInfo().getId())) continue; ban = new ModToolBan(h.getHabboInfo().getId(), h != null ? h.getHabboInfo().getIpLogin() : "offline", h != null ? h.getClient().getMachineId() : "offline", moderator.getHabboInfo().getId(), Emulator.getIntUnixTimestamp() + duration, reason, type, cfhTopic); Emulator.getPluginManager().fireEvent(new SupportUserBannedEvent(moderator, h, ban)); @@ -501,10 +506,27 @@ public class ModToolManager { if (targetInfo == null) return false; - return targetInfo.getRank().getId() < moderator.getHabboInfo().getRank().getId(); + int moderatorRankId = moderator.getHabboInfo().getRank().getId(); + int targetRankId = targetInfo.getRank().getId(); + + return targetRankId < moderatorRankId || isCoreRank(moderatorRankId) && targetRankId <= moderatorRankId; + } + + private static boolean isCoreRank(int rankId) { + int highestRankId = 0; + for (Rank rank : Emulator.getGameEnvironment().getPermissionsManager().getAllRanks()) { + highestRankId = Math.max(highestRankId, rank.getId()); + } + + return highestRankId > 0 && rankId >= highestRankId; } public void roomAction(Room room, Habbo moderator, boolean kickUsers, boolean lockDoor, boolean changeTitle) { + if (room == null || moderator == null || !moderator.hasPermission(Permission.ACC_SUPPORTTOOL) || + !canModerateTarget(moderator, room.getOwnerId())) { + return; + } + SupportRoomActionEvent roomActionEvent = new SupportRoomActionEvent(moderator, room, kickUsers, lockDoor, changeTitle); Emulator.getPluginManager().fireEvent(roomActionEvent); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolSanctions.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolSanctions.java index 14b2dfeb..85275631 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolSanctions.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/modtool/ModToolSanctions.java @@ -128,6 +128,10 @@ public class ModToolSanctions { } public void run(int habboId, Habbo self, int sanctionLevel, int cfhTopic, String reason, int tradeLockedUntil, boolean isMuted, int muteDuration) { + if (!ModToolManager.canModerateTarget(self, habboId)) { + return; + } + sanctionLevel++; ModToolSanctionLevelItem sanctionLevelItem = getSanctionLevelItem(sanctionLevel); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueDefaultSanctionEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueDefaultSanctionEvent.java index b237c5f5..2b0789f6 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueDefaultSanctionEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueDefaultSanctionEvent.java @@ -21,6 +21,10 @@ public class ModToolIssueDefaultSanctionEvent extends MessageHandler { ModToolIssue issue = Emulator.getGameEnvironment().getModToolManager().getTicket(issueId); + if (issue == null) { + return; + } + if (issue.modId == this.client.getHabbo().getHabboInfo().getId()) { CfhTopic modToolCategory = Emulator.getGameEnvironment().getModToolManager().getCfhTopic(category); diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolPermissionContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolPermissionContractTest.java index 460da889..9ab0ff8d 100644 --- a/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolPermissionContractTest.java +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolPermissionContractTest.java @@ -45,7 +45,7 @@ class ModToolPermissionContractTest { } @Test - void modToolSanctionsCannotTargetSameOrHigherRanks() throws Exception { + void modToolSanctionsCannotTargetPeerRanksUnlessOperatorIsCoreRank() throws Exception { Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool"); for (String handler : List.of( @@ -60,6 +60,30 @@ class ModToolPermissionContractTest { String manager = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java")); assertTrue(manager.contains("!canModerateTarget(moderator, target.getHabboInfo().getId())"), - "ModToolManager.alert must refuse alerts/warnings against same-or-higher-rank targets"); + "ModToolManager.alert must refuse alerts/warnings against protected targets"); + assertTrue(manager.contains("targetRankId < moderatorRankId"), + "non-core moderators must only target lower-ranked users"); + assertTrue(manager.contains("isCoreRank(moderatorRankId) && targetRankId <= moderatorRankId"), + "highest/core moderators should be allowed to target peer ranks"); + assertTrue(manager.contains("private static boolean isCoreRank(int rankId)"), + "core-rank detection should be centralized in ModToolManager"); + } + + @Test + void managerEntryPointsShareTargetAndRoomOwnerGuards() throws Exception { + String manager = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/modtool/ModToolManager.java")); + String sanctions = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/modtool/ModToolSanctions.java")); + String defaultSanction = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueDefaultSanctionEvent.java")); + + assertTrue(manager.contains("!canModerateTarget(moderator, targetUserId)"), + "ModToolManager.ban must use the central target-rank guard for offline and online users"); + assertTrue(manager.contains("!canModerateTarget(moderator, h.getHabboInfo().getId())"), + "IP and machine fan-out bans must skip protected peer-or-higher ranked sessions"); + assertTrue(manager.contains("!canModerateTarget(moderator, room.getOwnerId())"), + "ModToolManager.roomAction must refuse mutations on rooms owned by protected ranks"); + assertTrue(sanctions.contains("!ModToolManager.canModerateTarget(self, habboId)"), + "ModToolSanctions.run must guard every sanction path before writing or applying it"); + assertTrue(defaultSanction.contains("if (issue == null)"), + "default sanctions must tolerate stale or missing ticket ids"); } } From 8ba9132e7e9bf709691519c1c2e5ebdc350bacd3 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 15 Jun 2026 19:54:34 +0200 Subject: [PATCH 2/4] fix(modtool): bound staff supplied messages --- .../incoming/modtool/ModToolAlertEvent.java | 11 +++++++-- .../incoming/modtool/ModToolInputGuard.java | 16 +++++++++++++ .../incoming/modtool/ModToolKickEvent.java | 9 +++++++- .../modtool/ModToolRoomAlertEvent.java | 7 +++++- .../modtool/ModToolSanctionAlertEvent.java | 6 ++++- .../modtool/ModToolSanctionBanEvent.java | 8 +++++-- .../modtool/ModToolSanctionMuteEvent.java | 6 ++++- .../ModToolSanctionTradeLockEvent.java | 6 ++++- .../incoming/modtool/ModToolWarnEvent.java | 11 +++++++-- .../modtool/ModToolInputGuardTest.java | 23 +++++++++++++++++++ .../ModToolPermissionContractTest.java | 23 +++++++++++++++++++ 11 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolInputGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolAlertEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolAlertEvent.java index 09b4ebb3..c2b6820f 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolAlertEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolAlertEvent.java @@ -11,10 +11,17 @@ public class ModToolAlertEvent extends MessageHandler { @Override public void handle() throws Exception { if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { - Habbo alertedUser = Emulator.getGameEnvironment().getHabboManager().getHabbo(this.packet.readInt()); + int userId = this.packet.readInt(); + String message = ModToolInputGuard.normalize(this.packet.readString()); + + if (!ModToolInputGuard.isSafeMessage(message)) { + return; + } + + Habbo alertedUser = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); if (alertedUser != null) - Emulator.getGameEnvironment().getModToolManager().alert(this.client.getHabbo(), alertedUser, this.packet.readString(), SupportUserAlertedReason.ALERT); + Emulator.getGameEnvironment().getModToolManager().alert(this.client.getHabbo(), alertedUser, message, SupportUserAlertedReason.ALERT); } else { ScripterManager.scripterDetected(this.client, Emulator.getTexts().getValue("scripter.warning.modtools.kick").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername())); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolInputGuard.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolInputGuard.java new file mode 100644 index 00000000..f401a99a --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolInputGuard.java @@ -0,0 +1,16 @@ +package com.eu.habbo.messages.incoming.modtool; + +final class ModToolInputGuard { + static final int MAX_MESSAGE_LENGTH = 1000; + + private ModToolInputGuard() { + } + + static String normalize(String value) { + return value == null ? "" : value.trim(); + } + + static boolean isSafeMessage(String value) { + return value != null && !value.isEmpty() && value.length() <= MAX_MESSAGE_LENGTH; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolKickEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolKickEvent.java index 6491a4bf..da01445a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolKickEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolKickEvent.java @@ -18,6 +18,13 @@ public class ModToolKickEvent extends MessageHandler { return; } - Emulator.getGameEnvironment().getModToolManager().kick(this.client.getHabbo(), Emulator.getGameEnvironment().getHabboManager().getHabbo(this.packet.readInt()), this.packet.readString()); + int userId = this.packet.readInt(); + String message = ModToolInputGuard.normalize(this.packet.readString()); + + if (!ModToolInputGuard.isSafeMessage(message)) { + return; + } + + Emulator.getGameEnvironment().getModToolManager().kick(this.client.getHabbo(), Emulator.getGameEnvironment().getHabboManager().getHabbo(userId), message); } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRoomAlertEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRoomAlertEvent.java index 99f0bc0a..db84edf3 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRoomAlertEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRoomAlertEvent.java @@ -11,10 +11,15 @@ public class ModToolRoomAlertEvent extends MessageHandler { public void handle() throws Exception { if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { this.packet.readInt(); + String message = ModToolInputGuard.normalize(this.packet.readString()); + + if (!ModToolInputGuard.isSafeMessage(message)) { + return; + } Room room = this.client.getHabbo().getHabboInfo().getCurrentRoom(); if (room != null) { - room.alert(this.packet.readString()); + room.alert(message); } } else { ScripterManager.scripterDetected(this.client, Emulator.getTexts().getValue("scripter.warning.roomalert").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername())); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionAlertEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionAlertEvent.java index cb609001..c37f1f52 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionAlertEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionAlertEvent.java @@ -21,9 +21,13 @@ public class ModToolSanctionAlertEvent extends MessageHandler { @Override public void handle() throws Exception { int userId = this.packet.readInt(); - String message = this.packet.readString(); + String message = ModToolInputGuard.normalize(this.packet.readString()); int cfhTopic = this.packet.readInt(); + if (!ModToolInputGuard.isSafeMessage(message)) { + return; + } + if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionBanEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionBanEvent.java index 5a2c8afa..d23f943a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionBanEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionBanEvent.java @@ -30,13 +30,17 @@ public class ModToolSanctionBanEvent extends MessageHandler { @Override public void handle() throws Exception { int userId = this.packet.readInt(); - String message = this.packet.readString(); + String message = ModToolInputGuard.normalize(this.packet.readString()); int cfhTopic = this.packet.readInt(); int banType = this.packet.readInt(); this.packet.readBoolean(); int duration = 0; + if (!ModToolInputGuard.isSafeMessage(message)) { + return; + } + switch (banType) { case BAN_18_HOURS: duration = 18 * 60 * 60; @@ -79,4 +83,4 @@ public class ModToolSanctionBanEvent extends MessageHandler { ScripterManager.scripterDetected(this.client, Emulator.getTexts().getValue("scripter.warning.modtools.ban").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername())); } } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionMuteEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionMuteEvent.java index 16a2bbdd..e5f91ab2 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionMuteEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionMuteEvent.java @@ -23,9 +23,13 @@ public class ModToolSanctionMuteEvent extends MessageHandler { @Override public void handle() throws Exception { int userId = this.packet.readInt(); - String message = this.packet.readString(); + String message = ModToolInputGuard.normalize(this.packet.readString()); int cfhTopic = this.packet.readInt(); + if (!ModToolInputGuard.isSafeMessage(message)) { + return; + } + if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionTradeLockEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionTradeLockEvent.java index 9f666868..b49134f6 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionTradeLockEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolSanctionTradeLockEvent.java @@ -21,10 +21,14 @@ public class ModToolSanctionTradeLockEvent extends MessageHandler { @Override public void handle() throws Exception { int userId = this.packet.readInt(); - String message = this.packet.readString(); + String message = ModToolInputGuard.normalize(this.packet.readString()); int duration = this.packet.readInt(); int cfhTopic = this.packet.readInt(); + if (!ModToolInputGuard.isSafeMessage(message)) { + return; + } + if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolWarnEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolWarnEvent.java index a8dae64f..06c118b2 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolWarnEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolWarnEvent.java @@ -16,10 +16,17 @@ public class ModToolWarnEvent extends MessageHandler { @Override public void handle() throws Exception { if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { - Habbo alertedUser = Emulator.getGameEnvironment().getHabboManager().getHabbo(this.packet.readInt()); + int userId = this.packet.readInt(); + String message = ModToolInputGuard.normalize(this.packet.readString()); + + if (!ModToolInputGuard.isSafeMessage(message)) { + return; + } + + Habbo alertedUser = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); if (alertedUser != null) - Emulator.getGameEnvironment().getModToolManager().alert(this.client.getHabbo(), alertedUser, this.packet.readString(), SupportUserAlertedReason.CAUTION); + Emulator.getGameEnvironment().getModToolManager().alert(this.client.getHabbo(), alertedUser, message, SupportUserAlertedReason.CAUTION); } else { ScripterManager.scripterDetected(this.client, Emulator.getTexts().getValue("scripter.warning.modtools.kick").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername())); } diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolInputGuardTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolInputGuardTest.java new file mode 100644 index 00000000..214d2087 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolInputGuardTest.java @@ -0,0 +1,23 @@ +package com.eu.habbo.messages.incoming.modtool; + +import org.junit.jupiter.api.Test; + +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 ModToolInputGuardTest { + @Test + void normalizesNullableMessages() { + assertEquals("", ModToolInputGuard.normalize(null)); + assertEquals("warn", ModToolInputGuard.normalize(" warn ")); + } + + @Test + void staffMessagesMustBeNonEmptyAndBounded() { + assertFalse(ModToolInputGuard.isSafeMessage(null)); + assertFalse(ModToolInputGuard.isSafeMessage("")); + assertTrue(ModToolInputGuard.isSafeMessage("a".repeat(ModToolInputGuard.MAX_MESSAGE_LENGTH))); + assertFalse(ModToolInputGuard.isSafeMessage("a".repeat(ModToolInputGuard.MAX_MESSAGE_LENGTH + 1))); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolPermissionContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolPermissionContractTest.java index 9ab0ff8d..ba03fc67 100644 --- a/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolPermissionContractTest.java +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolPermissionContractTest.java @@ -86,4 +86,27 @@ class ModToolPermissionContractTest { assertTrue(defaultSanction.contains("if (issue == null)"), "default sanctions must tolerate stale or missing ticket ids"); } + + @Test + void staffSuppliedModToolMessagesAreBounded() throws Exception { + Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool"); + + for (String handler : List.of( + "ModToolAlertEvent.java", + "ModToolWarnEvent.java", + "ModToolKickEvent.java", + "ModToolRoomAlertEvent.java", + "ModToolSanctionAlertEvent.java", + "ModToolSanctionBanEvent.java", + "ModToolSanctionMuteEvent.java", + "ModToolSanctionTradeLockEvent.java" + )) { + String source = Files.readString(base.resolve(handler)); + + assertTrue(source.contains("ModToolInputGuard.normalize"), + handler + " must normalize staff-supplied text before use"); + assertTrue(source.contains("ModToolInputGuard.isSafeMessage"), + handler + " must reject empty or oversized staff-supplied text"); + } + } } From 044d1141cdbb86c4d9da1091298601ee29863fd6 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 15 Jun 2026 20:01:34 +0200 Subject: [PATCH 3/4] fix(modtool): validate report payloads --- .../modtool/ModToolReportInputGuard.java | 30 ++++++++ .../incoming/modtool/ReportBullyEvent.java | 6 +- .../incoming/modtool/ReportCommentEvent.java | 10 ++- .../incoming/modtool/ReportEvent.java | 20 ++++-- .../modtool/ReportFriendPrivateChatEvent.java | 20 +++++- .../incoming/modtool/ReportPhotoEvent.java | 6 ++ .../incoming/modtool/ReportThreadEvent.java | 9 ++- .../ModToolReportInputContractTest.java | 69 +++++++++++++++++++ .../modtool/ModToolReportInputGuardTest.java | 37 ++++++++++ 9 files changed, 196 insertions(+), 11 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolReportInputGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolReportInputContractTest.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolReportInputGuardTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolReportInputGuard.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolReportInputGuard.java new file mode 100644 index 00000000..f265096d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolReportInputGuard.java @@ -0,0 +1,30 @@ +package com.eu.habbo.messages.incoming.modtool; + +final class ModToolReportInputGuard { + static final int MAX_REPORT_MESSAGE_LENGTH = 1000; + static final int MAX_PRIVATE_CHAT_LOGS = 100; + static final int MAX_PRIVATE_CHAT_MESSAGE_LENGTH = 500; + + private ModToolReportInputGuard() { + } + + static String normalize(String value) { + return value == null ? "" : value.trim(); + } + + static boolean isValidReportMessage(String value) { + return value != null && !value.isEmpty() && value.length() <= MAX_REPORT_MESSAGE_LENGTH; + } + + static boolean isValidChatLogMessage(String value) { + return value != null && value.length() <= MAX_PRIVATE_CHAT_MESSAGE_LENGTH; + } + + static boolean isValidPrivateChatLogCount(int count) { + return count > 0 && count <= MAX_PRIVATE_CHAT_LOGS; + } + + static boolean isPositiveId(int id) { + return id > 0; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportBullyEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportBullyEvent.java index f2207f2c..7943cbc2 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportBullyEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportBullyEvent.java @@ -14,7 +14,7 @@ import java.util.ArrayList; public class ReportBullyEvent extends MessageHandler { @Override public void handle() throws Exception { - if (this.client.getHabbo().getHabboStats().allowTalk()) { + if (!this.client.getHabbo().getHabboStats().allowTalk()) { this.client.sendResponse(new HelperRequestDisabledComposer()); return; } @@ -22,7 +22,9 @@ public class ReportBullyEvent extends MessageHandler { int userId = this.packet.readInt(); int roomId = this.packet.readInt(); - if (userId == this.client.getHabbo().getHabboInfo().getId()) { + if (!ModToolReportInputGuard.isPositiveId(userId) || + !ModToolReportInputGuard.isPositiveId(roomId) || + userId == this.client.getHabbo().getHabboInfo().getId()) { return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportCommentEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportCommentEvent.java index b21849ba..85673001 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportCommentEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportCommentEvent.java @@ -17,7 +17,15 @@ public class ReportCommentEvent extends MessageHandler { int threadId = this.packet.readInt(); int commentId = this.packet.readInt(); int topicId = this.packet.readInt(); - String message = this.packet.readString(); + String message = ModToolReportInputGuard.normalize(this.packet.readString()); + + if (!ModToolReportInputGuard.isPositiveId(groupId) || + !ModToolReportInputGuard.isPositiveId(threadId) || + !ModToolReportInputGuard.isPositiveId(commentId) || + !ModToolReportInputGuard.isPositiveId(topicId) || + !ModToolReportInputGuard.isValidReportMessage(message)) { + return; + } CfhTopic topic = Emulator.getGameEnvironment().getModToolManager().getCfhTopic(topicId); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportEvent.java index 99c8ba6c..aabb2dbc 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportEvent.java @@ -21,12 +21,26 @@ public class ReportEvent extends MessageHandler { return; } - String message = this.packet.readString(); + String message = ModToolReportInputGuard.normalize(this.packet.readString()); int topic = this.packet.readInt(); int userId = this.packet.readInt(); int roomId = this.packet.readInt(); this.packet.readInt(); + if (!ModToolReportInputGuard.isValidReportMessage(message) || + topic <= 0 || + (userId != -1 && !ModToolReportInputGuard.isPositiveId(userId)) || + !ModToolReportInputGuard.isPositiveId(roomId) || + userId == this.client.getHabbo().getHabboInfo().getId()) { + return; + } + + CfhTopic cfhTopic = Emulator.getGameEnvironment().getModToolManager().getCfhTopic(topic); + + if (cfhTopic == null) { + return; + } + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId); List issues = Emulator.getGameEnvironment().getModToolManager().openTicketsForHabbo(this.client.getHabbo()); if (!issues.isEmpty()) { @@ -35,8 +49,6 @@ public class ReportEvent extends MessageHandler { return; } - CfhTopic cfhTopic = Emulator.getGameEnvironment().getModToolManager().getCfhTopic(topic); - if (userId != -1) { Habbo reported = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); @@ -117,4 +129,4 @@ public class ReportEvent extends MessageHandler { } } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportFriendPrivateChatEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportFriendPrivateChatEvent.java index 63e1ba43..4840679d 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportFriendPrivateChatEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportFriendPrivateChatEvent.java @@ -21,12 +21,20 @@ public class ReportFriendPrivateChatEvent extends MessageHandler { return; } - String message = this.packet.readString(); + String message = ModToolReportInputGuard.normalize(this.packet.readString()); int category = this.packet.readInt(); int userId = this.packet.readInt(); int count = this.packet.readInt(); ArrayList chatLogs = new ArrayList<>(); + if (!ModToolReportInputGuard.isValidReportMessage(message) || + category <= 0 || + !ModToolReportInputGuard.isPositiveId(userId) || + userId == this.client.getHabbo().getHabboInfo().getId() || + !ModToolReportInputGuard.isValidPrivateChatLogCount(count)) { + return; + } + HabboInfo info; Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); if (target != null) { @@ -37,11 +45,17 @@ public class ReportFriendPrivateChatEvent extends MessageHandler { if (info == null) return; - for (int i = 0; i < Math.min(count, 100); i++) { + for (int i = 0; i < count; i++) { int chatUserId = this.packet.readInt(); String username = this.packet.readInt() == info.getId() ? info.getUsername() : this.client.getHabbo().getHabboInfo().getUsername(); + String chatMessage = ModToolReportInputGuard.normalize(this.packet.readString()); - chatLogs.add(new ModToolChatLog(0, chatUserId, username, this.packet.readString())); + if (!ModToolReportInputGuard.isPositiveId(chatUserId) || + !ModToolReportInputGuard.isValidChatLogMessage(chatMessage)) { + return; + } + + chatLogs.add(new ModToolChatLog(0, chatUserId, username, chatMessage)); } ModToolIssue issue = new ModToolIssue(this.client.getHabbo().getHabboInfo().getId(), this.client.getHabbo().getHabboInfo().getUsername(), userId, info.getUsername(), 0, message, ModToolTicketType.IM); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportPhotoEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportPhotoEvent.java index fc225fb5..fc6e1f04 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportPhotoEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportPhotoEvent.java @@ -28,6 +28,12 @@ public class ReportPhotoEvent extends MessageHandler { int topicId = this.packet.readInt(); int itemId = this.packet.readInt(); + if (!ModToolReportInputGuard.isPositiveId(roomId) || + !ModToolReportInputGuard.isPositiveId(topicId) || + !ModToolReportInputGuard.isPositiveId(itemId)) { + return; + } + CfhTopic topic = Emulator.getGameEnvironment().getModToolManager().getCfhTopic(topicId); if (topic == null) return; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportThreadEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportThreadEvent.java index 50bcab89..b1e0b8b1 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportThreadEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ReportThreadEvent.java @@ -16,7 +16,14 @@ public class ReportThreadEvent extends MessageHandler { int groupId = this.packet.readInt(); int threadId = this.packet.readInt(); int topicId = this.packet.readInt(); - String message = this.packet.readString(); + String message = ModToolReportInputGuard.normalize(this.packet.readString()); + + if (!ModToolReportInputGuard.isPositiveId(groupId) || + !ModToolReportInputGuard.isPositiveId(threadId) || + !ModToolReportInputGuard.isPositiveId(topicId) || + !ModToolReportInputGuard.isValidReportMessage(message)) { + return; + } CfhTopic topic = Emulator.getGameEnvironment().getModToolManager().getCfhTopic(topicId); diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolReportInputContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolReportInputContractTest.java new file mode 100644 index 00000000..75f26644 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolReportInputContractTest.java @@ -0,0 +1,69 @@ +package com.eu.habbo.messages.incoming.modtool; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ModToolReportInputContractTest { + @Test + void reportHandlersNormalizeAndBoundFreeText() throws Exception { + Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool"); + + for (String handler : List.of( + "ReportEvent.java", + "ReportFriendPrivateChatEvent.java", + "ReportCommentEvent.java", + "ReportThreadEvent.java" + )) { + String source = Files.readString(base.resolve(handler)); + + assertTrue(source.contains("ModToolReportInputGuard.normalize"), + handler + " must normalize report text before persistence or staff broadcast"); + assertTrue(source.contains("ModToolReportInputGuard.isValidReportMessage"), + handler + " must reject empty or oversized report text"); + } + } + + @Test + void reportHandlersRejectInvalidIdsAndCounts() throws Exception { + Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool"); + + for (String handler : List.of( + "ReportEvent.java", + "ReportFriendPrivateChatEvent.java", + "ReportCommentEvent.java", + "ReportThreadEvent.java", + "ReportBullyEvent.java", + "ReportPhotoEvent.java" + )) { + String source = Files.readString(base.resolve(handler)); + + assertTrue(source.contains("ModToolReportInputGuard.isPositiveId"), + handler + " must reject zero or negative ids supplied by the client"); + } + + String privateChat = Files.readString(base.resolve("ReportFriendPrivateChatEvent.java")); + assertTrue(privateChat.contains("ModToolReportInputGuard.isValidPrivateChatLogCount(count)"), + "private chat reports must reject negative or oversized client-provided chatlog counts"); + } + + @Test + void reportEventValidatesTopicBeforeUsingReply() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool/ReportEvent.java")); + + assertTrue(source.indexOf("if (cfhTopic == null)") < source.indexOf("cfhTopic.reply"), + "ReportEvent must reject unknown topics before dereferencing the reply text"); + } + + @Test + void bullyReportUsesSameMutedUserGateAsNormalReports() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool/ReportBullyEvent.java")); + + assertTrue(source.contains("if (!this.client.getHabbo().getHabboStats().allowTalk())"), + "bully reports must reject muted users instead of rejecting users who can talk"); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolReportInputGuardTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolReportInputGuardTest.java new file mode 100644 index 00000000..91e49a46 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolReportInputGuardTest.java @@ -0,0 +1,37 @@ +package com.eu.habbo.messages.incoming.modtool; + +import org.junit.jupiter.api.Test; + +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 ModToolReportInputGuardTest { + @Test + void normalizesNullableMessages() { + assertEquals("", ModToolReportInputGuard.normalize(null)); + assertEquals("report", ModToolReportInputGuard.normalize(" report ")); + } + + @Test + void reportMessagesMustBeNonEmptyAndBounded() { + assertFalse(ModToolReportInputGuard.isValidReportMessage("")); + assertFalse(ModToolReportInputGuard.isValidReportMessage(null)); + assertTrue(ModToolReportInputGuard.isValidReportMessage("a".repeat(ModToolReportInputGuard.MAX_REPORT_MESSAGE_LENGTH))); + assertFalse(ModToolReportInputGuard.isValidReportMessage("a".repeat(ModToolReportInputGuard.MAX_REPORT_MESSAGE_LENGTH + 1))); + } + + @Test + void privateChatLogCountsAreBounded() { + assertFalse(ModToolReportInputGuard.isValidPrivateChatLogCount(0)); + assertTrue(ModToolReportInputGuard.isValidPrivateChatLogCount(ModToolReportInputGuard.MAX_PRIVATE_CHAT_LOGS)); + assertFalse(ModToolReportInputGuard.isValidPrivateChatLogCount(ModToolReportInputGuard.MAX_PRIVATE_CHAT_LOGS + 1)); + } + + @Test + void idsMustBePositive() { + assertFalse(ModToolReportInputGuard.isPositiveId(0)); + assertFalse(ModToolReportInputGuard.isPositiveId(-1)); + assertTrue(ModToolReportInputGuard.isPositiveId(1)); + } +} From 916ef7af3ade7760b21042fa1d1b897fcc873133 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 15 Jun 2026 20:07:24 +0200 Subject: [PATCH 4/4] fix(modtool): guard ticket lifecycle inputs --- .../modtool/ModToolCloseTicketEvent.java | 6 +- .../modtool/ModToolIssueChangeTopicEvent.java | 10 ++- .../modtool/ModToolPickTicketEvent.java | 11 ++- .../modtool/ModToolReleaseTicketEvent.java | 13 ++-- .../ModToolRequestIssueChatlogEvent.java | 8 ++- .../ModToolRequestRoomChatlogEvent.java | 8 ++- .../ModToolRequestRoomUserChatlogEvent.java | 4 ++ .../ModToolRequestUserChatlogEvent.java | 5 ++ .../incoming/modtool/ModToolTicketGuard.java | 28 ++++++++ .../modtool/ModToolTicketGuardTest.java | 22 ++++++ .../ModToolTicketLifecycleContractTest.java | 67 +++++++++++++++++++ 11 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolTicketGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolTicketGuardTest.java create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolTicketLifecycleContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolCloseTicketEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolCloseTicketEvent.java index 98320092..591b1de9 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolCloseTicketEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolCloseTicketEvent.java @@ -15,9 +15,13 @@ public class ModToolCloseTicketEvent extends MessageHandler { this.packet.readInt(); int ticketId = this.packet.readInt(); + if (!ModToolTicketGuard.isPositiveId(ticketId) || state < 1 || state > 3) { + return; + } + ModToolIssue issue = Emulator.getGameEnvironment().getModToolManager().getTicket(ticketId); - if (issue == null || issue.modId != this.client.getHabbo().getHabboInfo().getId()) + if (!ModToolTicketGuard.isOwnedBy(issue, this.client.getHabbo())) return; Habbo sender = Emulator.getGameEnvironment().getHabboManager().getHabbo(issue.senderId); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueChangeTopicEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueChangeTopicEvent.java index 64655536..e2c69701 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueChangeTopicEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueChangeTopicEvent.java @@ -14,9 +14,17 @@ public class ModToolIssueChangeTopicEvent extends MessageHandler { this.packet.readInt(); int categoryId = this.packet.readInt(); + if (!ModToolTicketGuard.isPositiveId(ticketId) || !ModToolTicketGuard.isPositiveId(categoryId)) { + return; + } + + if (Emulator.getGameEnvironment().getModToolManager().getCfhTopic(categoryId) == null) { + return; + } + ModToolIssue issue = Emulator.getGameEnvironment().getModToolManager().getTicket(ticketId); - if (issue != null) { + if (ModToolTicketGuard.isOwnedBy(issue, this.client.getHabbo())) { issue.category = categoryId; new UpdateModToolIssue(issue).run(); Emulator.getGameEnvironment().getModToolManager().updateTicketToMods(issue); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolPickTicketEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolPickTicketEvent.java index 172ad656..2bbd68ce 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolPickTicketEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolPickTicketEvent.java @@ -15,10 +15,17 @@ public class ModToolPickTicketEvent extends MessageHandler { public void handle() throws Exception { if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { this.packet.readInt(); - ModToolIssue issue = Emulator.getGameEnvironment().getModToolManager().getTicket(this.packet.readInt()); + int ticketId = this.packet.readInt(); + + if (!ModToolTicketGuard.isPositiveId(ticketId)) { + this.client.getHabbo().alert(Emulator.getTexts().getValue("support.ticket.picked.failed")); + return; + } + + ModToolIssue issue = Emulator.getGameEnvironment().getModToolManager().getTicket(ticketId); if (issue != null) { - if (issue.state == ModToolTicketState.PICKED) { + if (!ModToolTicketGuard.canPick(issue)) { this.client.sendResponse(new ModToolIssueInfoComposer(issue)); this.client.getHabbo().alert(Emulator.getTexts().getValue("support.ticket.picked.failed")); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolReleaseTicketEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolReleaseTicketEvent.java index e0a155d9..cc08c125 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolReleaseTicketEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolReleaseTicketEvent.java @@ -13,17 +13,22 @@ public class ModToolReleaseTicketEvent extends MessageHandler { if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { int count = this.packet.readInt(); + if (!ModToolTicketGuard.isValidReleaseBatch(count)) { + return; + } + while (count != 0) { count--; int ticketId = this.packet.readInt(); + if (!ModToolTicketGuard.isPositiveId(ticketId)) { + continue; + } + ModToolIssue issue = Emulator.getGameEnvironment().getModToolManager().getTicket(ticketId); - if (issue == null) - continue; - - if (issue.modId != this.client.getHabbo().getHabboInfo().getId()) + if (!ModToolTicketGuard.isOwnedBy(issue, this.client.getHabbo())) continue; issue.modId = 0; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestIssueChatlogEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestIssueChatlogEvent.java index 4eb2756e..0bc5df4d 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestIssueChatlogEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestIssueChatlogEvent.java @@ -16,7 +16,13 @@ public class ModToolRequestIssueChatlogEvent extends MessageHandler { @Override public void handle() throws Exception { if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { - ModToolIssue issue = Emulator.getGameEnvironment().getModToolManager().getTicket(this.packet.readInt()); + int ticketId = this.packet.readInt(); + + if (!ModToolTicketGuard.isPositiveId(ticketId)) { + return; + } + + ModToolIssue issue = Emulator.getGameEnvironment().getModToolManager().getTicket(ticketId); if (issue != null) { List chatlog = new ArrayList<>(); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestRoomChatlogEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestRoomChatlogEvent.java index 5f58f50e..2e2e3de1 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestRoomChatlogEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestRoomChatlogEvent.java @@ -12,7 +12,13 @@ public class ModToolRequestRoomChatlogEvent extends MessageHandler { public void handle() throws Exception { if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { this.packet.readInt(); - Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(this.packet.readInt()); + int roomId = this.packet.readInt(); + + if (!ModToolTicketGuard.isPositiveId(roomId)) { + return; + } + + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId); if (room != null) this.client.sendResponse(new ModToolRoomChatlogComposer(room, Emulator.getGameEnvironment().getModToolManager().getRoomChatlog(room.getId()))); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestRoomUserChatlogEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestRoomUserChatlogEvent.java index 6fec0a90..a674ab4c 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestRoomUserChatlogEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestRoomUserChatlogEvent.java @@ -14,6 +14,10 @@ public class ModToolRequestRoomUserChatlogEvent extends MessageHandler { if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { int userId = this.packet.readInt(); + if (!ModToolTicketGuard.isPositiveId(userId)) { + return; + } + Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId); if (habbo != null) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestUserChatlogEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestUserChatlogEvent.java index a175cdd0..f4ab8040 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestUserChatlogEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolRequestUserChatlogEvent.java @@ -13,6 +13,11 @@ public class ModToolRequestUserChatlogEvent extends MessageHandler { public void handle() throws Exception { if (this.client.getHabbo().hasPermission(Permission.ACC_SUPPORTTOOL)) { int userId = this.packet.readInt(); + + if (!ModToolTicketGuard.isPositiveId(userId)) { + return; + } + HabboInfo habboInfo = HabboManager.getOfflineHabboInfo(userId); if (habboInfo == null) { return; diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolTicketGuard.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolTicketGuard.java new file mode 100644 index 00000000..a5844c6d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolTicketGuard.java @@ -0,0 +1,28 @@ +package com.eu.habbo.messages.incoming.modtool; + +import com.eu.habbo.habbohotel.modtool.ModToolIssue; +import com.eu.habbo.habbohotel.modtool.ModToolTicketState; +import com.eu.habbo.habbohotel.users.Habbo; + +final class ModToolTicketGuard { + static final int MAX_RELEASE_BATCH = 50; + + private ModToolTicketGuard() { + } + + static boolean isPositiveId(int id) { + return id > 0; + } + + static boolean isValidReleaseBatch(int count) { + return count > 0 && count <= MAX_RELEASE_BATCH; + } + + static boolean isOwnedBy(ModToolIssue issue, Habbo moderator) { + return issue != null && moderator != null && issue.modId == moderator.getHabboInfo().getId(); + } + + static boolean canPick(ModToolIssue issue) { + return issue != null && issue.state != ModToolTicketState.PICKED; + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolTicketGuardTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolTicketGuardTest.java new file mode 100644 index 00000000..a6255d16 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolTicketGuardTest.java @@ -0,0 +1,22 @@ +package com.eu.habbo.messages.incoming.modtool; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ModToolTicketGuardTest { + @Test + void idsMustBePositive() { + assertFalse(ModToolTicketGuard.isPositiveId(0)); + assertFalse(ModToolTicketGuard.isPositiveId(-1)); + assertTrue(ModToolTicketGuard.isPositiveId(1)); + } + + @Test + void releaseBatchIsBounded() { + assertFalse(ModToolTicketGuard.isValidReleaseBatch(0)); + assertTrue(ModToolTicketGuard.isValidReleaseBatch(ModToolTicketGuard.MAX_RELEASE_BATCH)); + assertFalse(ModToolTicketGuard.isValidReleaseBatch(ModToolTicketGuard.MAX_RELEASE_BATCH + 1)); + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolTicketLifecycleContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolTicketLifecycleContractTest.java new file mode 100644 index 00000000..b74a7aee --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/modtool/ModToolTicketLifecycleContractTest.java @@ -0,0 +1,67 @@ +package com.eu.habbo.messages.incoming.modtool; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ModToolTicketLifecycleContractTest { + @Test + void mutatingTicketActionsValidateOwnership() throws Exception { + Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool"); + + for (String handler : List.of( + "ModToolCloseTicketEvent.java", + "ModToolIssueChangeTopicEvent.java", + "ModToolReleaseTicketEvent.java" + )) { + String source = Files.readString(base.resolve(handler)); + + assertTrue(source.contains("ModToolTicketGuard.isOwnedBy(issue, this.client.getHabbo())"), + handler + " must only mutate tickets owned by the acting moderator"); + } + } + + @Test + void clientDrivenTicketAndChatlogIdsAreValidated() throws Exception { + Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool"); + + for (String handler : List.of( + "ModToolPickTicketEvent.java", + "ModToolCloseTicketEvent.java", + "ModToolIssueChangeTopicEvent.java", + "ModToolRequestIssueChatlogEvent.java", + "ModToolRequestRoomChatlogEvent.java", + "ModToolRequestRoomUserChatlogEvent.java", + "ModToolRequestUserChatlogEvent.java" + )) { + String source = Files.readString(base.resolve(handler)); + + assertTrue(source.contains("ModToolTicketGuard.isPositiveId"), + handler + " must reject zero or negative client-provided ids"); + } + } + + @Test + void releaseBatchAndCloseStateAreBounded() throws Exception { + Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool"); + String release = Files.readString(base.resolve("ModToolReleaseTicketEvent.java")); + String close = Files.readString(base.resolve("ModToolCloseTicketEvent.java")); + + assertTrue(release.contains("ModToolTicketGuard.isValidReleaseBatch(count)"), + "release ticket batches must be bounded before reading ticket ids"); + assertTrue(close.contains("state < 1 || state > 3"), + "close ticket must reject unknown close states before mutating the ticket"); + } + + @Test + void changeTopicRequiresKnownCategory() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/modtool/ModToolIssueChangeTopicEvent.java")); + + assertTrue(source.contains("getCfhTopic(categoryId) == null"), + "change-topic must reject unknown CFH categories before persisting"); + } +}