fix(rcon): validate grant requests

This commit is contained in:
simoleo89
2026-06-14 16:45:04 +02:00
parent 3cb24a5185
commit dba0337a7b
11 changed files with 237 additions and 1 deletions
@@ -21,6 +21,18 @@ public class GiveCredits extends RCONMessage<GiveCredits.JSONGiveCredits> {
@Override
public void handle(Gson gson, JSONGiveCredits object) {
int maxAmount = RconGrantGuard.parseMaxAmount(
Emulator.getConfig().getValue("rcon.grant.max_amount", String.valueOf(RconGrantGuard.DEFAULT_MAX_AMOUNT)));
String validationError = RconGrantGuard.validateUserId(object.user_id);
if (validationError == null) {
validationError = RconGrantGuard.validatePositiveAmount(object.credits, maxAmount, "credits");
}
if (validationError != null) {
this.status = RCONMessage.STATUS_ERROR;
this.message = validationError;
return;
}
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(object.user_id);
if (habbo != null) {
@@ -21,11 +21,29 @@ public class GivePixels extends RCONMessage<GivePixels.JSONGivePixels> {
@Override
public void handle(Gson gson, JSONGivePixels object) {
int maxAmount = RconGrantGuard.parseMaxAmount(
Emulator.getConfig().getValue("rcon.grant.max_amount", String.valueOf(RconGrantGuard.DEFAULT_MAX_AMOUNT)));
String validationError = RconGrantGuard.validateUserId(object.user_id);
if (validationError == null) {
validationError = RconGrantGuard.validatePositiveAmount(object.pixels, maxAmount, "pixels");
}
if (validationError != null) {
this.status = RCONMessage.STATUS_ERROR;
this.message = validationError;
return;
}
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(object.user_id);
if (habbo != null) {
habbo.givePixels(object.pixels);
} else {
if (!RconUserLookup.userExists(object.user_id)) {
this.status = RCONMessage.HABBO_NOT_FOUND;
this.message = "user not found";
return;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_currency (`user_id`, `type`, `amount`) VALUES (?, 0, ?) ON DUPLICATE KEY UPDATE amount = amount + ?")) {
statement.setInt(1, object.user_id);
statement.setInt(2, object.pixels);
@@ -22,11 +22,32 @@ public class GivePoints extends RCONMessage<GivePoints.JSONGivePoints> {
@Override
public void handle(Gson gson, JSONGivePoints object) {
int maxAmount = RconGrantGuard.parseMaxAmount(
Emulator.getConfig().getValue("rcon.grant.max_amount", String.valueOf(RconGrantGuard.DEFAULT_MAX_AMOUNT)));
String validationError = RconGrantGuard.validateUserId(object.user_id);
if (validationError == null) {
validationError = RconGrantGuard.validateCurrencyType(object.type);
}
if (validationError == null) {
validationError = RconGrantGuard.validatePositiveAmount(object.points, maxAmount, "points");
}
if (validationError != null) {
this.status = RCONMessage.STATUS_ERROR;
this.message = validationError;
return;
}
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(object.user_id);
if (habbo != null) {
habbo.givePoints(object.type, object.points);
} else {
if (!RconUserLookup.userExists(object.user_id)) {
this.status = RCONMessage.HABBO_NOT_FOUND;
this.message = "user not found";
return;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO users_currency (`user_id`, `type`, `amount`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE amount = amount + ?")) {
statement.setInt(1, object.user_id);
statement.setInt(2, object.type);
@@ -21,6 +21,27 @@ public class GiveRespect extends RCONMessage<GiveRespect.JSONGiveRespect> {
@Override
public void handle(Gson gson, JSONGiveRespect object) {
int maxAmount = RconGrantGuard.parseMaxAmount(
Emulator.getConfig().getValue("rcon.grant.max_amount", String.valueOf(RconGrantGuard.DEFAULT_MAX_AMOUNT)));
String validationError = RconGrantGuard.validateUserId(object.user_id);
if (validationError == null) {
validationError = RconGrantGuard.validateNonNegativeAmount(object.respect_given, maxAmount, "respect_given");
}
if (validationError == null) {
validationError = RconGrantGuard.validateNonNegativeAmount(object.respect_received, maxAmount, "respect_received");
}
if (validationError == null) {
validationError = RconGrantGuard.validateNonNegativeAmount(object.daily_respects, maxAmount, "daily_respects");
}
if (validationError == null && object.respect_given == 0 && object.respect_received == 0 && object.daily_respects == 0) {
validationError = "no respect grant provided";
}
if (validationError != null) {
this.status = RCONMessage.STATUS_ERROR;
this.message = validationError;
return;
}
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(object.user_id);
if (habbo != null) {
@@ -29,15 +50,26 @@ public class GiveRespect extends RCONMessage<GiveRespect.JSONGiveRespect> {
habbo.getHabboStats().respectPointsToGive += object.daily_respects;
habbo.getClient().sendResponse(new UserDataComposer(habbo));
} else {
if (!RconUserLookup.userExists(object.user_id)) {
this.status = RCONMessage.HABBO_NOT_FOUND;
this.message = "user not found";
return;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET respects_given = respects_given + ?, respects_received = respects_received + ?, daily_respect_points = daily_respect_points + ? WHERE user_id = ? LIMIT 1")) {
statement.setInt(1, object.respect_given);
statement.setInt(2, object.respect_received);
statement.setInt(3, object.daily_respects);
statement.setInt(4, object.user_id);
statement.execute();
if (statement.executeUpdate() == 0) {
this.status = RCONMessage.HABBO_NOT_FOUND;
this.message = "user not found";
return;
}
} catch (SQLException e) {
this.status = RCONMessage.SYSTEM_ERROR;
LOGGER.error("Caught SQL exception", e);
return;
}
this.message = "offline";
@@ -0,0 +1,49 @@
package com.eu.habbo.messages.rcon;
public class RconGrantGuard {
public static final int DEFAULT_MAX_AMOUNT = 1_000_000;
private RconGrantGuard() {
}
public static String validateUserId(int userId) {
return userId > 0 ? null : "invalid user";
}
public static String validatePositiveAmount(int amount, int maxAmount, String fieldName) {
if (amount <= 0) {
return "invalid " + fieldName;
}
if (maxAmount > 0 && amount > maxAmount) {
return fieldName + " exceeds rcon grant ceiling";
}
return null;
}
public static String validateNonNegativeAmount(int amount, int maxAmount, String fieldName) {
if (amount < 0) {
return "invalid " + fieldName;
}
if (maxAmount > 0 && amount > maxAmount) {
return fieldName + " exceeds rcon grant ceiling";
}
return null;
}
public static String validateCurrencyType(int type) {
return type >= 0 ? null : "invalid currency type";
}
public static int parseMaxAmount(String rawValue) {
try {
int value = Integer.parseInt(rawValue);
return value > 0 ? value : DEFAULT_MAX_AMOUNT;
} catch (Exception e) {
return DEFAULT_MAX_AMOUNT;
}
}
}
@@ -0,0 +1,28 @@
package com.eu.habbo.messages.rcon;
import com.eu.habbo.Emulator;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class RconUserLookup {
private RconUserLookup() {
}
public static boolean userExists(int userId) {
if (Emulator.getGameEnvironment().getHabboManager().getHabbo(userId) != null) {
return true;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT id FROM users WHERE id = ? LIMIT 1")) {
statement.setInt(1, userId);
try (ResultSet set = statement.executeQuery()) {
return set.next();
}
} catch (Exception e) {
return false;
}
}
}
@@ -20,5 +20,7 @@ class GiveCreditsContractTest {
"Offline RCON credit grants must inspect the affected row count");
assertTrue(source.contains("HABBO_NOT_FOUND"),
"Offline RCON credit grants must report missing users when the UPDATE changes no rows");
assertTrue(source.contains("RconGrantGuard.validatePositiveAmount"),
"RCON credit grants must reject zero, negative, and oversized grants");
}
}
@@ -20,5 +20,9 @@ class GivePixelsContractTest {
"Offline RCON pixel grants must create the users_currency type 0 row when it is missing");
assertTrue(source.contains("ON DUPLICATE KEY UPDATE"),
"Offline RCON pixel grants should increment existing rows with an upsert");
assertTrue(source.contains("RconGrantGuard.validatePositiveAmount"),
"RCON pixel grants must reject zero, negative, and oversized grants");
assertTrue(source.contains("RconUserLookup.userExists"),
"Offline RCON pixel grants must not create orphan currency rows for missing users");
}
}
@@ -0,0 +1,28 @@
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.assertTrue;
class GivePointsContractTest {
private static String givePointsSource() throws Exception {
return Files.readString(Path.of("src/main/java/com/eu/habbo/messages/rcon/GivePoints.java"));
}
@Test
void pointGrantsValidateAmountTypeAndOfflineUserExistence() throws Exception {
String source = givePointsSource();
assertTrue(source.contains("RconGrantGuard.validateCurrencyType"),
"RCON point grants must reject invalid currency types");
assertTrue(source.contains("RconGrantGuard.validatePositiveAmount"),
"RCON point grants must reject zero, negative, and oversized grants");
assertTrue(source.contains("RconUserLookup.userExists"),
"Offline RCON point grants must not create orphan currency rows for missing users");
assertTrue(source.contains("ON DUPLICATE KEY UPDATE"),
"Offline RCON point grants should increment existing rows with an upsert");
}
}
@@ -20,5 +20,9 @@ class GiveRespectContractTest {
"respects_given must be incremented with respect_given");
assertTrue(source.contains("statement.setInt(2, object.respect_received);"),
"respects_received must be incremented with respect_received");
assertTrue(source.contains("RconGrantGuard.validateNonNegativeAmount"),
"RCON respect grants must reject negative values");
assertTrue(source.contains("statement.executeUpdate() == 0"),
"Offline RCON respect grants must report missing users when the UPDATE changes no rows");
}
}
@@ -0,0 +1,38 @@
package com.eu.habbo.messages.rcon;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
class RconGrantGuardTest {
@Test
void validatesPositiveGrantAmounts() {
assertNull(RconGrantGuard.validatePositiveAmount(1, 100, "credits"));
assertEquals("invalid credits", RconGrantGuard.validatePositiveAmount(0, 100, "credits"));
assertEquals("invalid credits", RconGrantGuard.validatePositiveAmount(-1, 100, "credits"));
assertEquals("credits exceeds rcon grant ceiling", RconGrantGuard.validatePositiveAmount(101, 100, "credits"));
}
@Test
void validatesNonNegativeGrantAmounts() {
assertNull(RconGrantGuard.validateNonNegativeAmount(0, 100, "respect_given"));
assertEquals("invalid respect_given", RconGrantGuard.validateNonNegativeAmount(-1, 100, "respect_given"));
assertEquals("respect_given exceeds rcon grant ceiling", RconGrantGuard.validateNonNegativeAmount(101, 100, "respect_given"));
}
@Test
void validatesUserAndCurrencyIdentifiers() {
assertNull(RconGrantGuard.validateUserId(1));
assertEquals("invalid user", RconGrantGuard.validateUserId(0));
assertNull(RconGrantGuard.validateCurrencyType(0));
assertEquals("invalid currency type", RconGrantGuard.validateCurrencyType(-1));
}
@Test
void parsesInvalidGrantCeilingsAsDefault() {
assertEquals(RconGrantGuard.DEFAULT_MAX_AMOUNT, RconGrantGuard.parseMaxAmount(null));
assertEquals(RconGrantGuard.DEFAULT_MAX_AMOUNT, RconGrantGuard.parseMaxAmount("0"));
assertEquals(500, RconGrantGuard.parseMaxAmount("500"));
}
}