diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java index 4d6e49f4..4168ec95 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingBanUserEvent.java @@ -30,10 +30,10 @@ public class HousekeepingBanUserEvent extends MessageHandler { } int userId = this.packet.readInt(); - String reason = this.packet.readString(); + String reason = HousekeepingInputGuard.normalize(this.packet.readString()); int hours = this.packet.readInt(); - if (userId <= 0 || hours <= 0) { + if (userId <= 0 || hours <= 0 || !HousekeepingInputGuard.isWithinLimit(reason, HousekeepingInputGuard.MAX_REASON_LENGTH)) { this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); return; } @@ -46,7 +46,7 @@ public class HousekeepingBanUserEvent extends MessageHandler { int duration = HousekeepingSanctionDuration.secondsFromHours(hours); List bans = Emulator.getGameEnvironment().getModToolManager() - .ban(userId, this.client.getHabbo(), reason != null ? reason : "", duration, ModToolBanType.ACCOUNT, 0); + .ban(userId, this.client.getHabbo(), reason, duration, ModToolBanType.ACCOUNT, 0); if (bans == null || bans.isEmpty()) { this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.ban_failed")); @@ -60,7 +60,7 @@ public class HousekeepingBanUserEvent extends MessageHandler { com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log( this.client.getHabbo().getHabboInfo().getId(), this.client.getHabbo().getHabboInfo().getUsername(), - ACTION_KEY, userId, "hours=" + hours + " reason=" + (reason != null ? reason : ""), + ACTION_KEY, userId, "hours=" + hours + " reason=" + HousekeepingInputGuard.auditValue(reason), this.client.getHabbo().getHabboInfo().getIpLogin()); this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByNameEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByNameEvent.java index 070733c6..f226f725 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByNameEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingFindUserByNameEvent.java @@ -20,9 +20,9 @@ public class HousekeepingFindUserByNameEvent extends MessageHandler { return; } - String username = this.packet.readString(); + String username = HousekeepingInputGuard.normalize(this.packet.readString()); - if (username == null || username.isEmpty()) { + if (username.isEmpty() || !HousekeepingInputGuard.isWithinLimit(username, HousekeepingInputGuard.MAX_LOOKUP_LENGTH)) { this.client.sendResponse(new HousekeepingUserDetailComposer(null)); return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingForceDisconnectUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingForceDisconnectUserEvent.java index 785523b3..7639f1df 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingForceDisconnectUserEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingForceDisconnectUserEvent.java @@ -26,9 +26,9 @@ public class HousekeepingForceDisconnectUserEvent extends MessageHandler { } int userId = this.packet.readInt(); - String reason = this.packet.readString(); + String reason = HousekeepingInputGuard.normalize(this.packet.readString()); - if (userId <= 0) { + if (userId <= 0 || !HousekeepingInputGuard.isWithinLimit(reason, HousekeepingInputGuard.MAX_REASON_LENGTH)) { this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); return; } @@ -45,7 +45,7 @@ public class HousekeepingForceDisconnectUserEvent extends MessageHandler { return; } - if (reason != null && !reason.isEmpty()) { + if (!reason.isEmpty()) { target.alert(reason); } @@ -55,7 +55,7 @@ public class HousekeepingForceDisconnectUserEvent extends MessageHandler { com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log( this.client.getHabbo().getHabboInfo().getId(), this.client.getHabbo().getHabboInfo().getUsername(), - ACTION_KEY, userId, "reason=" + (reason != null ? reason : ""), + ACTION_KEY, userId, "reason=" + HousekeepingInputGuard.auditValue(reason), this.client.getHabbo().getHabboInfo().getIpLogin()); this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingInputGuard.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingInputGuard.java new file mode 100644 index 00000000..b6232506 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingInputGuard.java @@ -0,0 +1,27 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +final class HousekeepingInputGuard { + static final int MAX_LOOKUP_LENGTH = 64; + static final int MAX_REASON_LENGTH = 500; + static final int MAX_ALERT_LENGTH = 1000; + + private HousekeepingInputGuard() { + } + + static String normalize(String value) { + return value == null ? "" : value.trim(); + } + + static boolean isWithinLimit(String value, int maxLength) { + return value != null && value.length() <= maxLength; + } + + static String auditValue(String value) { + String normalized = normalize(value) + .replace('\r', ' ') + .replace('\n', ' ') + .replace('\t', ' '); + + return normalized.length() > MAX_REASON_LENGTH ? normalized.substring(0, MAX_REASON_LENGTH) : normalized; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickUserEvent.java index 93ed2992..60e15f54 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickUserEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingKickUserEvent.java @@ -28,9 +28,9 @@ public class HousekeepingKickUserEvent extends MessageHandler { } int userId = this.packet.readInt(); - String reason = this.packet.readString(); + String reason = HousekeepingInputGuard.normalize(this.packet.readString()); - if (userId <= 0) { + if (userId <= 0 || !HousekeepingInputGuard.isWithinLimit(reason, HousekeepingInputGuard.MAX_REASON_LENGTH)) { this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); return; } @@ -56,14 +56,14 @@ public class HousekeepingKickUserEvent extends MessageHandler { Emulator.getGameEnvironment().getRoomManager().leaveRoom(target, target.getHabboInfo().getCurrentRoom()); } - if (reason != null && !reason.isEmpty()) { + if (!reason.isEmpty()) { target.alert(reason); } com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log( this.client.getHabbo().getHabboInfo().getId(), this.client.getHabbo().getHabboInfo().getUsername(), - ACTION_KEY, userId, "reason=" + (reason != null ? reason : ""), + ACTION_KEY, userId, "reason=" + HousekeepingInputGuard.auditValue(reason), this.client.getHabbo().getHabboInfo().getIpLogin()); this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java index ac019305..4fe6bd31 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingMuteUserEvent.java @@ -28,10 +28,10 @@ public class HousekeepingMuteUserEvent extends MessageHandler { } int userId = this.packet.readInt(); - String reason = this.packet.readString(); + String reason = HousekeepingInputGuard.normalize(this.packet.readString()); int minutes = this.packet.readInt(); - if (userId <= 0 || minutes <= 0) { + if (userId <= 0 || minutes <= 0 || !HousekeepingInputGuard.isWithinLimit(reason, HousekeepingInputGuard.MAX_REASON_LENGTH)) { this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); return; } @@ -50,14 +50,14 @@ public class HousekeepingMuteUserEvent extends MessageHandler { target.mute(HousekeepingSanctionDuration.secondsFromMinutes(minutes), false); - if (reason != null && !reason.isEmpty()) { + if (!reason.isEmpty()) { target.alert(reason); } com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log( this.client.getHabbo().getHabboInfo().getId(), this.client.getHabbo().getHabboInfo().getUsername(), - ACTION_KEY, userId, "minutes=" + minutes + " reason=" + (reason != null ? reason : ""), + ACTION_KEY, userId, "minutes=" + minutes + " reason=" + HousekeepingInputGuard.auditValue(reason), this.client.getHabbo().getHabboInfo().getIpLogin()); this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSearchRoomsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSearchRoomsEvent.java index bccf6588..db45e999 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSearchRoomsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSearchRoomsEvent.java @@ -36,14 +36,11 @@ public class HousekeepingSearchRoomsEvent extends MessageHandler { return; } - String query = this.packet.readString(); + String query = HousekeepingInputGuard.normalize(this.packet.readString()); boolean exactMatch = this.packet.readBoolean(); int limit = Math.min(Math.max(this.packet.readInt(), 1), HARD_LIMIT); - if (query == null) query = ""; - query = query.trim(); - - if (query.isEmpty()) { + if (query.isEmpty() || !HousekeepingInputGuard.isWithinLimit(query, HousekeepingInputGuard.MAX_LOOKUP_LENGTH)) { this.client.sendResponse(new HousekeepingRoomListComposer(new ArrayList<>())); return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSendHotelAlertEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSendHotelAlertEvent.java index 0f397c35..7c2c9c49 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSendHotelAlertEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingSendHotelAlertEvent.java @@ -31,13 +31,18 @@ public class HousekeepingSendHotelAlertEvent extends MessageHandler { return; } - String message = this.packet.readString(); + String message = HousekeepingInputGuard.normalize(this.packet.readString()); - if (message == null || message.trim().isEmpty()) { + if (message.isEmpty()) { this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.alert_empty")); return; } + if (!HousekeepingInputGuard.isWithinLimit(message, HousekeepingInputGuard.MAX_ALERT_LENGTH)) { + this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.input_too_long")); + return; + } + String body = message + "\r\n-" + this.client.getHabbo().getHabboInfo().getUsername(); ServerMessage broadcast = new StaffAlertWithLinkComposer(body, "").compose(); @@ -56,7 +61,7 @@ public class HousekeepingSendHotelAlertEvent extends MessageHandler { com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log( this.client.getHabbo().getHabboInfo().getId(), this.client.getHabbo().getHabboInfo().getUsername(), - ACTION_KEY, 0, "reached=" + reached + " message=" + message, + ACTION_KEY, 0, "reached=" + reached + " message=" + HousekeepingInputGuard.auditValue(message), this.client.getHabbo().getHabboInfo().getIpLogin()); this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, reached, "")); } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java index 0b284115..98b1ddbe 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingTradeLockUserEvent.java @@ -34,9 +34,9 @@ public class HousekeepingTradeLockUserEvent extends MessageHandler { int userId = this.packet.readInt(); int hours = this.packet.readInt(); - String reason = this.packet.readString(); + String reason = HousekeepingInputGuard.normalize(this.packet.readString()); - if (userId <= 0 || hours <= 0) { + if (userId <= 0 || hours <= 0 || !HousekeepingInputGuard.isWithinLimit(reason, HousekeepingInputGuard.MAX_REASON_LENGTH)) { this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, false, 0, "housekeeping.error.invalid_input")); return; } @@ -69,7 +69,7 @@ public class HousekeepingTradeLockUserEvent extends MessageHandler { if (online != null) { online.getHabboStats().setAllowTrade(false); - if (reason != null && !reason.isEmpty()) { + if (!reason.isEmpty()) { online.alert(reason); } } @@ -77,7 +77,7 @@ public class HousekeepingTradeLockUserEvent extends MessageHandler { com.eu.habbo.habbohotel.modtool.HousekeepingAuditLog.log( this.client.getHabbo().getHabboInfo().getId(), this.client.getHabbo().getHabboInfo().getUsername(), - ACTION_KEY, userId, "hours=" + hours + " lockedUntil=" + lockedUntil + " reason=" + (reason != null ? reason : ""), + ACTION_KEY, userId, "hours=" + hours + " lockedUntil=" + lockedUntil + " reason=" + HousekeepingInputGuard.auditValue(reason), this.client.getHabbo().getHabboInfo().getIpLogin()); this.client.sendResponse(new HousekeepingActionResultComposer(ACTION_KEY, true, userId, "")); } diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingInputGuardContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingInputGuardContractTest.java new file mode 100644 index 00000000..4db64e28 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingInputGuardContractTest.java @@ -0,0 +1,53 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +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 HousekeepingInputGuardContractTest { + @Test + void stringDrivenHousekeepingHandlersUseSharedLimits() throws Exception { + Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping"); + + for (String handler : List.of( + "HousekeepingBanUserEvent.java", + "HousekeepingForceDisconnectUserEvent.java", + "HousekeepingKickUserEvent.java", + "HousekeepingMuteUserEvent.java", + "HousekeepingTradeLockUserEvent.java", + "HousekeepingSendHotelAlertEvent.java", + "HousekeepingSearchRoomsEvent.java", + "HousekeepingFindUserByNameEvent.java" + )) { + String source = Files.readString(base.resolve(handler)); + + assertTrue(source.contains("HousekeepingInputGuard.normalize"), + handler + " must normalize client-provided strings before use"); + assertTrue(source.contains("HousekeepingInputGuard.isWithinLimit"), + handler + " must bound client-provided strings before expensive work or broadcast"); + } + } + + @Test + void auditedFreeTextIsSanitizedBeforePersistence() throws Exception { + Path base = Path.of("src/main/java/com/eu/habbo/messages/incoming/housekeeping"); + + for (String handler : List.of( + "HousekeepingBanUserEvent.java", + "HousekeepingForceDisconnectUserEvent.java", + "HousekeepingKickUserEvent.java", + "HousekeepingMuteUserEvent.java", + "HousekeepingTradeLockUserEvent.java", + "HousekeepingSendHotelAlertEvent.java" + )) { + String source = Files.readString(base.resolve(handler)); + + assertTrue(source.contains("HousekeepingInputGuard.auditValue"), + handler + " must collapse control whitespace before writing free text to audit detail"); + } + } +} diff --git a/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingInputGuardTest.java b/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingInputGuardTest.java new file mode 100644 index 00000000..8e73304b --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/incoming/housekeeping/HousekeepingInputGuardTest.java @@ -0,0 +1,32 @@ +package com.eu.habbo.messages.incoming.housekeeping; + +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 HousekeepingInputGuardTest { + @Test + void normalizesNullableText() { + assertEquals("", HousekeepingInputGuard.normalize(null)); + assertEquals("hello", HousekeepingInputGuard.normalize(" hello ")); + } + + @Test + void enforcesInclusiveLengthLimits() { + assertTrue(HousekeepingInputGuard.isWithinLimit("abc", 3)); + assertFalse(HousekeepingInputGuard.isWithinLimit("abcd", 3)); + assertFalse(HousekeepingInputGuard.isWithinLimit(null, 3)); + } + + @Test + void auditValuesCollapseControlWhitespaceAndCapLength() { + String value = HousekeepingInputGuard.auditValue(" one\r\ntwo\tthree "); + + assertEquals("one two three", value); + + String oversized = "x".repeat(HousekeepingInputGuard.MAX_REASON_LENGTH + 1); + assertEquals(HousekeepingInputGuard.MAX_REASON_LENGTH, HousekeepingInputGuard.auditValue(oversized).length()); + } +}