From 775197984f73527783f8e04a9d6e087bc30b38b3 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sat, 13 Jun 2026 15:33:38 +0200 Subject: [PATCH] fix(rcon): validate offline badge targets GiveBadge could treat a missing offline user as eligible for a badge and insert through a nullable user subquery. Depending on SQL mode this could fail late or persist an orphaned user_id value. Resolve the offline user first, return HABBO_NOT_FOUND when absent, and insert badges with the resolved user id only. --- .../com/eu/habbo/messages/rcon/GiveBadge.java | 17 +++++++++--- .../messages/rcon/GiveBadgeContractTest.java | 27 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/messages/rcon/GiveBadgeContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/messages/rcon/GiveBadge.java b/Emulator/src/main/java/com/eu/habbo/messages/rcon/GiveBadge.java index f233b32c..dcca9cc9 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/rcon/GiveBadge.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/rcon/GiveBadge.java @@ -3,6 +3,8 @@ package com.eu.habbo.messages.rcon; import com.eu.habbo.Emulator; import com.eu.habbo.habbohotel.users.Habbo; import com.eu.habbo.habbohotel.users.HabboBadge; +import com.eu.habbo.habbohotel.users.HabboInfo; +import com.eu.habbo.habbohotel.users.HabboManager; import com.eu.habbo.messages.outgoing.users.AddUserBadgeComposer; import com.google.gson.Gson; import jakarta.validation.constraints.NotBlank; @@ -57,11 +59,18 @@ public class GiveBadge extends RCONMessage { this.message = Emulator.getTexts().getValue("commands.succes.cmd_badge.given").replace("%user%", username).replace("%badge%", badgeCode); } } else { + HabboInfo habboInfo = HabboManager.getOfflineHabboInfo(json.user_id); + if (habboInfo == null) { + this.status = RCONMessage.HABBO_NOT_FOUND; + return; + } + + username = habboInfo.getUsername(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { for (String badgeCode : json.badge.split(";")) { int numberOfRows = 0; - try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(slot_id) FROM users_badges INNER JOIN users ON users.id = user_id WHERE users.id = ? AND badge_code = ? LIMIT 1")) { - statement.setInt(1, json.user_id); + try (PreparedStatement statement = connection.prepareStatement("SELECT COUNT(slot_id) FROM users_badges WHERE user_id = ? AND badge_code = ? LIMIT 1")) { + statement.setInt(1, habboInfo.getId()); statement.setString(2, badgeCode); try (ResultSet set = statement.executeQuery()) { if (set.next()){ @@ -74,8 +83,8 @@ public class GiveBadge extends RCONMessage { this.status = RCONMessage.STATUS_ERROR; this.message += Emulator.getTexts().getValue("commands.error.cmd_badge.already_owns").replace("%user%", username).replace("%badge%", badgeCode) + "\r"; } else { - try (PreparedStatement statement = connection.prepareStatement("INSERT INTO users_badges VALUES (null, (SELECT id FROM users WHERE users.id = ? LIMIT 1), 0, ?)", Statement.RETURN_GENERATED_KEYS)) { - statement.setInt(1, json.user_id); + try (PreparedStatement statement = connection.prepareStatement("INSERT INTO users_badges (`id`, `user_id`, `slot_id`, `badge_code`) VALUES (null, ?, 0, ?)", Statement.RETURN_GENERATED_KEYS)) { + statement.setInt(1, habboInfo.getId()); statement.setString(2, badgeCode); statement.execute(); } diff --git a/Emulator/src/test/java/com/eu/habbo/messages/rcon/GiveBadgeContractTest.java b/Emulator/src/test/java/com/eu/habbo/messages/rcon/GiveBadgeContractTest.java new file mode 100644 index 00000000..aaba384a --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/messages/rcon/GiveBadgeContractTest.java @@ -0,0 +1,27 @@ +package com.eu.habbo.messages.rcon; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GiveBadgeContractTest { + private static String giveBadgeSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/GiveBadge.java")); + } + + @Test + void offlineBadgeGrantRequiresExistingUserBeforeInsert() throws Exception { + String source = giveBadgeSource(); + + assertTrue(source.contains("HabboManager.getOfflineHabboInfo(json.user_id)"), + "Offline RCON badge grants must verify the target user exists"); + assertTrue(source.contains("RCONMessage.HABBO_NOT_FOUND"), + "Offline RCON badge grants must report missing users"); + assertFalse(source.contains("(SELECT id FROM users WHERE users.id = ? LIMIT 1)"), + "Offline RCON badge grants must not insert through a nullable user subquery"); + } +}