From 032003b64c4e5336300c8f3120836c4a8250e361 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 21:25:55 +0200 Subject: [PATCH] fix(commands): enforce staff target ceilings --- .../habbohotel/commands/AlertCommand.java | 5 ++ .../habbo/habbohotel/commands/BanCommand.java | 2 +- .../commands/CommandTargetGuard.java | 46 +++++++++++++ .../commands/DisconnectCommand.java | 2 +- .../commands/GivePrefixCommand.java | 5 ++ .../habbohotel/commands/GiveRankCommand.java | 6 +- .../habbohotel/commands/IPBanCommand.java | 2 +- .../commands/MachineBanCommand.java | 2 +- .../habbohotel/commands/MuteCommand.java | 5 ++ .../commands/RemovePrefixCommand.java | 5 ++ .../habbohotel/commands/SuperbanCommand.java | 4 +- .../habbohotel/commands/UnmuteCommand.java | 5 ++ .../CommandTargetGuardContractTest.java | 64 +++++++++++++++++++ 13 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandTargetGuard.java create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/commands/CommandTargetGuardContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/AlertCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/AlertCommand.java index 17c2042f..eec564c1 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/AlertCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/AlertCommand.java @@ -32,6 +32,11 @@ public class AlertCommand extends Command { Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(targetUsername); if (habbo != null) { + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); + return true; + } + habbo.alert(message + "\r\n -" + gameClient.getHabbo().getHabboInfo().getUsername()); gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_alert.message_send").replace("%user%", targetUsername), RoomChatMessageBubbles.ALERT); } else { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/BanCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/BanCommand.java index 90b2ea57..61e66d50 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/BanCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/BanCommand.java @@ -60,7 +60,7 @@ public class BanCommand extends Command { return true; } - if (target.getRank().getId() >= gameClient.getHabbo().getHabboInfo().getRank().getId()) { + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), target)) { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); return true; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandTargetGuard.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandTargetGuard.java new file mode 100644 index 00000000..cd7b97b3 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/CommandTargetGuard.java @@ -0,0 +1,46 @@ +package com.eu.habbo.habbohotel.commands; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.permissions.Rank; +import com.eu.habbo.habbohotel.users.Habbo; +import com.eu.habbo.habbohotel.users.HabboInfo; + +final class CommandTargetGuard { + private CommandTargetGuard() { + } + + static boolean canTarget(Habbo moderator, Habbo target) { + return target != null && canTarget(moderator, target.getHabboInfo()); + } + + static boolean canTarget(Habbo moderator, HabboInfo target) { + if (moderator == null || target == null || moderator.getHabboInfo().getId() == target.getId()) { + return false; + } + + int moderatorRankId = moderator.getHabboInfo().getRank().getId(); + int targetRankId = target.getRank().getId(); + + return targetRankId < moderatorRankId || isCoreRank(moderatorRankId) && targetRankId <= moderatorRankId; + } + + static boolean canAssignRank(Habbo moderator, Rank rank) { + if (moderator == null || rank == null) { + return false; + } + + int moderatorRankId = moderator.getHabboInfo().getRank().getId(); + int targetRankId = rank.getId(); + + return targetRankId < moderatorRankId || isCoreRank(moderatorRankId) && targetRankId <= moderatorRankId; + } + + private static boolean isCoreRank(int rankId) { + int highestRankId = Emulator.getGameEnvironment().getPermissionsManager().getAllRanks().stream() + .mapToInt(Rank::getId) + .max() + .orElse(0); + + return highestRankId > 0 && rankId >= highestRankId; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/DisconnectCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/DisconnectCommand.java index 82dd200e..b7f0eed8 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/DisconnectCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/DisconnectCommand.java @@ -29,7 +29,7 @@ public class DisconnectCommand extends Command { return true; } - if (target.getHabboInfo().getRank().getId() > gameClient.getHabbo().getHabboInfo().getRank().getId()) { + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), target)) { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_disconnect.higher_rank"), RoomChatMessageBubbles.ALERT); return true; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GivePrefixCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GivePrefixCommand.java index eba65376..30201488 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GivePrefixCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GivePrefixCommand.java @@ -47,6 +47,11 @@ public class GivePrefixCommand extends Command { return true; } + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), target)) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); + return true; + } + UserPrefix prefix = new UserPrefix(target.getHabboInfo().getId(), text, color, icon, effect); prefix.run(); target.getInventory().getPrefixesComponent().addPrefix(prefix); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GiveRankCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GiveRankCommand.java index bfe0cfb1..12a8c247 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GiveRankCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/GiveRankCommand.java @@ -36,7 +36,7 @@ public class GiveRankCommand extends Command { } if (rank != null) { - if (rank.getId() > gameClient.getHabbo().getHabboInfo().getRank().getId()) { + if (!CommandTargetGuard.canAssignRank(gameClient.getHabbo(), rank)) { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_rank.higher").replace("%username%", params[1]).replace("%id%", rank.getName()), RoomChatMessageBubbles.ALERT); return true; } @@ -44,7 +44,7 @@ public class GiveRankCommand extends Command { HabboInfo habbo = HabboManager.getOfflineHabboInfo(params[1]); if (habbo != null) { - if (habbo.getRank().getId() > gameClient.getHabbo().getHabboInfo().getRank().getId()) { + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_give_rank.higher.other").replace("%username%", params[1]).replace("%id%", rank.getName()), RoomChatMessageBubbles.ALERT); return true; } @@ -63,4 +63,4 @@ public class GiveRankCommand extends Command { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.errors.cmd_give_rank.not_found").replace("%id%", params[2]).replace("%username%", params[1]), RoomChatMessageBubbles.ALERT); return true; } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/IPBanCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/IPBanCommand.java index 8622cec9..8c9af239 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/IPBanCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/IPBanCommand.java @@ -47,7 +47,7 @@ public class IPBanCommand extends Command { return true; } - if (habbo.getRank().getId() >= gameClient.getHabbo().getHabboInfo().getRank().getId()) { + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); return true; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MachineBanCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MachineBanCommand.java index 29918bb9..5fa15c9f 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MachineBanCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MachineBanCommand.java @@ -43,7 +43,7 @@ public class MachineBanCommand extends Command { return true; } - if (habbo.getRank().getId() >= gameClient.getHabbo().getHabboInfo().getRank().getId()) { + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); return true; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MuteCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MuteCommand.java index 222c8cf3..8b1682fb 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MuteCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/MuteCommand.java @@ -29,6 +29,11 @@ public class MuteCommand extends Command { return true; } + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); + return true; + } + int duration = Integer.MAX_VALUE; if (params.length == 3) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RemovePrefixCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RemovePrefixCommand.java index 9396d18d..ddf8d024 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RemovePrefixCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/RemovePrefixCommand.java @@ -31,6 +31,11 @@ public class RemovePrefixCommand extends Command { return true; } + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), target)) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); + return true; + } + if (prefixIdStr.equalsIgnoreCase("all")) { List prefixes = target.getInventory().getPrefixesComponent().getPrefixes(); for (UserPrefix prefix : prefixes) { diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/SuperbanCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/SuperbanCommand.java index 8f9d8968..1a4d44e3 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/SuperbanCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/SuperbanCommand.java @@ -41,7 +41,7 @@ public class SuperbanCommand extends Command { return true; } - if (habbo.getRank().getId() >= gameClient.getHabbo().getHabboInfo().getRank().getId()) { + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); return true; } @@ -56,4 +56,4 @@ public class SuperbanCommand extends Command { return true; } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/UnmuteCommand.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/UnmuteCommand.java index 7cd922e9..ba563dd5 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/UnmuteCommand.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/commands/UnmuteCommand.java @@ -23,6 +23,11 @@ public class UnmuteCommand extends Command { gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_unmute.not_found").replace("%user%", params[1]), RoomChatMessageBubbles.ALERT); return true; } else { + if (!CommandTargetGuard.canTarget(gameClient.getHabbo(), habbo)) { + gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_ban.target_rank_higher"), RoomChatMessageBubbles.ALERT); + return true; + } + if (!habbo.getHabboStats().allowTalk() || (habbo.getHabboInfo().getCurrentRoom() != null && habbo.getHabboInfo().getCurrentRoom().isMuted(habbo))) { if (!habbo.getHabboStats().allowTalk()) { habbo.unMute(); diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/commands/CommandTargetGuardContractTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/commands/CommandTargetGuardContractTest.java new file mode 100644 index 00000000..57aa369e --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/commands/CommandTargetGuardContractTest.java @@ -0,0 +1,64 @@ +package com.eu.habbo.habbohotel.commands; + +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 CommandTargetGuardContractTest { + @Test + void highRiskUserCommandsUseCentralTargetGuard() throws Exception { + Path base = Path.of("src/main/java/com/eu/habbo/habbohotel/commands"); + + for (String command : List.of( + "AlertCommand.java", + "BanCommand.java", + "DisconnectCommand.java", + "GivePrefixCommand.java", + "GiveRankCommand.java", + "IPBanCommand.java", + "MachineBanCommand.java", + "MuteCommand.java", + "RemovePrefixCommand.java", + "SuperbanCommand.java", + "UnmuteCommand.java" + )) { + String source = Files.readString(base.resolve(command)); + + assertTrue(source.contains("CommandTargetGuard.canTarget"), + command + " must use the central command target guard for staff/core rank handling"); + } + } + + @Test + void rankGrantingUsesCentralAssignmentGuard() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/commands/GiveRankCommand.java")); + + assertTrue(source.contains("CommandTargetGuard.canAssignRank"), + "GiveRankCommand must guard the assigned rank with the same core-rank semantics"); + } + + @Test + void targetGuardKeepsCorePeerOverrideCentralized() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/commands/CommandTargetGuard.java")); + String rule = "targetRankId < moderatorRankId || isCoreRank(moderatorRankId) && targetRankId <= moderatorRankId"; + + assertTrue(countOccurrences(source, rule) >= 2, + "non-core command users must only target lower ranks while the highest/core rank may target peer ranks"); + } + + private static int countOccurrences(String source, String needle) { + int count = 0; + int index = 0; + + while ((index = source.indexOf(needle, index)) >= 0) { + count++; + index += needle.length(); + } + + return count; + } +}