You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
feat: LIKE-wildcard escaping (security) + recycle/craft reward rollback (stability)
Security / speed: - New util SqlLikeEscaper: escapes %, _ and \ in user search input. Applied to the user-facing LIKE searches (messenger user search, marketplace search, furni-editor search, housekeeping room search, guild member search) so a query like "%" can no longer match everything or trigger a needless full scan, and usernames containing "_" are matched literally. Stability (item-loss fixes): - RecycleEvent: compute the recycler reward BEFORE consuming the 8 inputs. The inputs were deleted from the DB first, so a null reward (misconfig) destroyed them permanently with nothing back. Now the inputs are only removed once the reward is confirmed. - CraftingCraftItemEvent: restore the pulled ingredients to the inventory if the recipe can't be completed (not enough ingredients mid-pull, or reward creation returns null) — previously they silently vanished from the inventory.
This commit is contained in:
@@ -171,8 +171,9 @@ public class MarketPlace {
|
|||||||
statement.setInt(paramIndex++, maxPrice);
|
statement.setInt(paramIndex++, maxPrice);
|
||||||
}
|
}
|
||||||
if (!search.isEmpty()) {
|
if (!search.isEmpty()) {
|
||||||
statement.setString(paramIndex++, "%" + search + "%");
|
String likeSearch = "%" + com.eu.habbo.util.SqlLikeEscaper.escape(search) + "%";
|
||||||
statement.setString(paramIndex++, "%" + search + "%");
|
statement.setString(paramIndex++, likeSearch);
|
||||||
|
statement.setString(paramIndex++, likeSearch);
|
||||||
}
|
}
|
||||||
|
|
||||||
try (ResultSet set = statement.executeQuery()) {
|
try (ResultSet set = statement.executeQuery()) {
|
||||||
|
|||||||
@@ -421,7 +421,7 @@ public class GuildManager {
|
|||||||
|
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT users.username, users.look, guilds_members.* FROM guilds_members INNER JOIN users ON guilds_members.user_id = users.id WHERE guilds_members.guild_id = ? " + (rankQuery(levelId)) + " AND users.username LIKE ? ORDER BY level_id, member_since ASC LIMIT ?, ?")) {
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT users.username, users.look, guilds_members.* FROM guilds_members INNER JOIN users ON guilds_members.user_id = users.id WHERE guilds_members.guild_id = ? " + (rankQuery(levelId)) + " AND users.username LIKE ? ORDER BY level_id, member_since ASC LIMIT ?, ?")) {
|
||||||
statement.setInt(1, guild.getId());
|
statement.setInt(1, guild.getId());
|
||||||
statement.setString(2, "%" + query + "%");
|
statement.setString(2, "%" + com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%");
|
||||||
statement.setInt(3, page * 14);
|
statement.setInt(3, page * 14);
|
||||||
statement.setInt(4, 14);
|
statement.setInt(4, 14);
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class Messenger {
|
|||||||
public static THashSet<MessengerBuddy> searchUsers(String username) {
|
public static THashSet<MessengerBuddy> searchUsers(String username) {
|
||||||
THashSet<MessengerBuddy> users = new THashSet<>();
|
THashSet<MessengerBuddy> users = new THashSet<>();
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE username LIKE ? ORDER BY username ASC LIMIT " + Emulator.getConfig().getInt("hotel.messenger.search.maxresults"))) {
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT * FROM users WHERE username LIKE ? ORDER BY username ASC LIMIT " + Emulator.getConfig().getInt("hotel.messenger.search.maxresults"))) {
|
||||||
statement.setString(1, username + "%");
|
statement.setString(1, com.eu.habbo.util.SqlLikeEscaper.escape(username) + "%");
|
||||||
try (ResultSet set = statement.executeQuery()) {
|
try (ResultSet set = statement.executeQuery()) {
|
||||||
while (set.next()) {
|
while (set.next()) {
|
||||||
users.add(new MessengerBuddy(set, false));
|
users.add(new MessengerBuddy(set, false));
|
||||||
|
|||||||
+10
-7
@@ -40,23 +40,26 @@ public class RecycleEvent extends MessageHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.size() == count) {
|
if (items.size() != count) {
|
||||||
for (HabboItem item : items) {
|
|
||||||
this.client.getHabbo().getInventory().getItemsComponent().removeHabboItem(item);
|
|
||||||
this.client.sendResponse(new RemoveHabboItemComposer(item.getGiftAdjustedId()));
|
|
||||||
Emulator.getThreading().run(new QueryDeleteHabboItem(item.getId()));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute the reward BEFORE consuming the inputs. Previously the
|
||||||
|
// inputs were deleted first, so a null reward (misconfiguration)
|
||||||
|
// permanently destroyed the 8 furni with nothing in return.
|
||||||
HabboItem reward = Emulator.getGameEnvironment().getItemManager().handleRecycle(this.client.getHabbo(), Emulator.getGameEnvironment().getCatalogManager().getRandomRecyclerPrize().getId() + "");
|
HabboItem reward = Emulator.getGameEnvironment().getItemManager().handleRecycle(this.client.getHabbo(), Emulator.getGameEnvironment().getCatalogManager().getRandomRecyclerPrize().getId() + "");
|
||||||
if (reward == null) {
|
if (reward == null) {
|
||||||
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (HabboItem item : items) {
|
||||||
|
this.client.getHabbo().getInventory().getItemsComponent().removeHabboItem(item);
|
||||||
|
this.client.sendResponse(new RemoveHabboItemComposer(item.getGiftAdjustedId()));
|
||||||
|
Emulator.getThreading().run(new QueryDeleteHabboItem(item.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
this.client.sendResponse(new AddHabboItemComposer(reward));
|
this.client.sendResponse(new AddHabboItemComposer(reward));
|
||||||
this.client.getHabbo().getInventory().getItemsComponent().addItem(reward);
|
this.client.getHabbo().getInventory().getItemsComponent().addItem(reward);
|
||||||
this.client.sendResponse(new RecyclerCompleteComposer(RecyclerCompleteComposer.RECYCLING_COMPLETE));
|
this.client.sendResponse(new RecyclerCompleteComposer(RecyclerCompleteComposer.RECYCLING_COMPLETE));
|
||||||
|
|||||||
+17
@@ -37,6 +37,8 @@ public class CraftingCraftItemEvent extends MessageHandler {
|
|||||||
HabboItem habboItem = this.client.getHabbo().getInventory().getItemsComponent().getAndRemoveHabboItem(set.getKey());
|
HabboItem habboItem = this.client.getHabbo().getInventory().getItemsComponent().getAndRemoveHabboItem(set.getKey());
|
||||||
|
|
||||||
if (habboItem == null) {
|
if (habboItem == null) {
|
||||||
|
// Not enough ingredients — give back whatever we already pulled.
|
||||||
|
this.restoreItems(toRemove);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,8 +72,23 @@ public class CraftingCraftItemEvent extends MessageHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reward creation failed after we already pulled the ingredients —
|
||||||
|
// restore them so the craft isn't a silent item sink.
|
||||||
|
this.restoreItems(toRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.client.sendResponse(new CraftingResultComposer(null));
|
this.client.sendResponse(new CraftingResultComposer(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void restoreItems(TIntObjectHashMap<HabboItem> items) {
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
items.forEachValue(item -> {
|
||||||
|
this.client.getHabbo().getInventory().getItemsComponent().addItem(item);
|
||||||
|
this.client.sendResponse(new AddHabboItemComposer(item));
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
this.client.sendResponse(new InventoryRefreshComposer());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -49,15 +49,17 @@ public class FurniEditorSearchEvent extends MessageHandler {
|
|||||||
try {
|
try {
|
||||||
int numericQuery = Integer.parseInt(query);
|
int numericQuery = Integer.parseInt(query);
|
||||||
isNumeric = true;
|
isNumeric = true;
|
||||||
|
String likeQuery = "%" + com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%";
|
||||||
whereClause.append(" AND (id = ? OR sprite_id = ? OR item_name LIKE ? OR public_name LIKE ?)");
|
whereClause.append(" AND (id = ? OR sprite_id = ? OR item_name LIKE ? OR public_name LIKE ?)");
|
||||||
params.add(numericQuery);
|
params.add(numericQuery);
|
||||||
params.add(numericQuery);
|
params.add(numericQuery);
|
||||||
params.add("%" + query + "%");
|
params.add(likeQuery);
|
||||||
params.add("%" + query + "%");
|
params.add(likeQuery);
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
|
String likeQuery = "%" + com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%";
|
||||||
whereClause.append(" AND (item_name LIKE ? OR public_name LIKE ?)");
|
whereClause.append(" AND (item_name LIKE ? OR public_name LIKE ?)");
|
||||||
params.add("%" + query + "%");
|
params.add(likeQuery);
|
||||||
params.add("%" + query + "%");
|
params.add(likeQuery);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -56,7 +56,7 @@ public class HousekeepingSearchRoomsEvent extends MessageHandler {
|
|||||||
|
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||||
statement.setString(1, exactMatch ? query : query + "%");
|
statement.setString(1, exactMatch ? query : com.eu.habbo.util.SqlLikeEscaper.escape(query) + "%");
|
||||||
statement.setInt(2, limit);
|
statement.setInt(2, limit);
|
||||||
|
|
||||||
try (ResultSet set = statement.executeQuery()) {
|
try (ResultSet set = statement.executeQuery()) {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.eu.habbo.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes the LIKE wildcards {@code %} and {@code _} (and the escape char itself)
|
||||||
|
* in user-supplied search input, so they are matched literally instead of acting
|
||||||
|
* as wildcards. Prevents wildcard-driven over-broad matches and the expensive
|
||||||
|
* full-scans an attacker could trigger with a query like {@code "%"}. Uses
|
||||||
|
* MariaDB's default escape character {@code \}.
|
||||||
|
*/
|
||||||
|
public final class SqlLikeEscaper {
|
||||||
|
|
||||||
|
private SqlLikeEscaper() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String escape(String input) {
|
||||||
|
if (input == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("%", "\\%")
|
||||||
|
.replace("_", "\\_");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user