From 31027095ec622c42a7332a268106ae5d777c5a26 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 14 Jun 2026 21:55:19 +0200 Subject: [PATCH] fix(housekeeping): enforce rank ceilings on rank changes --- .../HousekeepingSetUserRankEvent.java | 15 +++++++-------- .../HousekeepingTargetRankGuard.java | 13 ++++++++++++- .../HousekeepingTargetRankGuardContractTest.java | 16 ++++++++++++++++ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java index db100eb8..6fba7de9 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java @@ -45,13 +45,7 @@ public class HousekeepingSetUserRankEvent extends MessageHandler { Rank rank = permissions.getRank(rankId); - // Rank-ceiling guard: an operator must never be able to grant a rank - // above their own, nor modify a user who already outranks them. This - // mirrors GiveRankCommand and prevents privilege escalation through - // the housekeeping path (including self-promotion). - int operatorRankId = this.client.getHabbo().getHabboInfo().getRank().getId(); - - if (rank.getId() > operatorRankId) { + if (!HousekeepingTargetRankGuard.canAssignRank(this.client.getHabbo(), rank.getId())) { this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high")); return; } @@ -77,7 +71,12 @@ public class HousekeepingSetUserRankEvent extends MessageHandler { } } - if (targetRankId > operatorRankId) { + if (targetRankId <= 0) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.user_not_found")); + return; + } + + if (!HousekeepingTargetRankGuard.canTargetRank(this.client.getHabbo(), targetRankId)) { this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.rank_too_high")); return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTargetRankGuard.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTargetRankGuard.java index e5623a7d..3488829a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTargetRankGuard.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTargetRankGuard.java @@ -19,12 +19,23 @@ final class HousekeepingTargetRankGuard { return true; } + return canTargetRank(operator, targetInfo.getRank().getId()); + } + + static boolean canTargetRank(Habbo operator, int targetRankId) { + if (operator == null || targetRankId <= 0) { + return false; + } + int operatorRankId = operator.getHabboInfo().getRank().getId(); - int targetRankId = targetInfo.getRank().getId(); return targetRankId < operatorRankId || isCoreRank(operatorRankId) && targetRankId <= operatorRankId; } + static boolean canAssignRank(Habbo operator, int rankId) { + return canTargetRank(operator, rankId); + } + private static boolean isCoreRank(int rankId) { int highestRankId = 0; for (Rank rank : Emulator.getGameEnvironment().getPermissionsManager().getAllRanks()) { diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTargetRankGuardContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTargetRankGuardContractTest.java index eaa3fc5f..56ffd75d 100644 --- a/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTargetRankGuardContractTest.java +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTargetRankGuardContractTest.java @@ -27,6 +27,10 @@ class HousekeepingTargetRankGuardContractTest { void privilegedUserActionsRejectPeerRanksUnlessOperatorIsCoreRank() throws Exception { String guard = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTargetRankGuard.java")); + assertTrue(guard.contains("static boolean canTargetRank(Habbo operator, int targetRankId)"), + "rank comparison should be reusable for online and offline housekeeping targets"); + assertTrue(guard.contains("static boolean canAssignRank(Habbo operator, int rankId)"), + "rank assignment should use the same peer/core ceiling as target moderation"); assertTrue(guard.contains("targetRankId < operatorRankId"), "non-core housekeeping operators must only target lower-ranked users"); assertTrue(guard.contains("isCoreRank(operatorRankId) && targetRankId <= operatorRankId"), @@ -47,4 +51,16 @@ class HousekeepingTargetRankGuardContractTest { handler + " must return a rank-ceiling error when the target cannot be managed"); } } + + @Test + void housekeepingRankChangesUseCentralRankCeilings() throws Exception { + String source = Files.readString(Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSetUserRankEvent.java")); + + assertTrue(source.contains("HousekeepingTargetRankGuard.canAssignRank(this.client.getHabbo(), rank.getId())"), + "housekeeping rank assignment must not grant peer-or-higher ranks to non-core operators"); + assertTrue(source.contains("HousekeepingTargetRankGuard.canTargetRank(this.client.getHabbo(), targetRankId)"), + "housekeeping rank assignment must not modify peer-or-higher ranked targets for non-core operators"); + assertTrue(source.contains("housekeeping.error.user_not_found"), + "rank changes must reject missing offline users instead of reporting success for a zero-row update"); + } }