diff --git a/Emulator/src/main/java/com/eu/habbo/Emulator.java b/Emulator/src/main/java/com/eu/habbo/Emulator.java index 52c7f8b1..ea27325a 100644 --- a/Emulator/src/main/java/com/eu/habbo/Emulator.java +++ b/Emulator/src/main/java/com/eu/habbo/Emulator.java @@ -166,6 +166,7 @@ public final class Emulator { Emulator.config.register("rcon.rate_limit.timeout_ms", "0"); Emulator.config.register("rcon.execute_command.denied_permissions", "cmd_shutdown;cmd_give_rank"); Emulator.config.register("rcon.execute_command.allowed_permissions", ""); + Emulator.config.register("rcon.max_payload_bytes", "65536"); registerEarningsSettings(); String hotelTimezoneId = Emulator.getConfig().getValue("hotel.timezone", java.time.ZoneId.systemDefault().getId()); System.out.println(startupCard(hotelTimezoneId)); diff --git a/Emulator/src/main/java/com/eu/habbo/networking/rconserver/RCONServerHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/rconserver/RCONServerHandler.java index 014ba0ec..7c81c050 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/rconserver/RCONServerHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/rconserver/RCONServerHandler.java @@ -12,6 +12,9 @@ import io.netty.channel.ChannelInboundHandlerAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.InetSocketAddress; +import java.net.SocketAddress; + public class RCONServerHandler extends ChannelInboundHandlerAdapter { private static final Logger LOGGER = LoggerFactory.getLogger(RCONServerHandler.class); @@ -19,20 +22,21 @@ public class RCONServerHandler extends ChannelInboundHandlerAdapter { // Gson is thread-safe and immutable once built — share one instance instead // of allocating a parser per RCON request. private static final Gson GSON = new Gson(); + private static final int DEFAULT_MAX_PAYLOAD_BYTES = 64 * 1024; @Override public void channelRegistered(ChannelHandlerContext ctx) throws Exception { - String adress = ctx.channel().remoteAddress().toString().split(":")[0].replace("/", ""); + String address = remoteAddress(ctx); for (String s : Emulator.getRconServer().allowedAdresses) { - if (s.equalsIgnoreCase(adress)) { + if (s.equalsIgnoreCase(address)) { return; } } ctx.channel().close(); - LOGGER.warn("RCON Remote connection closed: {}. IP not allowed!", adress); + LOGGER.warn("RCON Remote connection closed: {}. IP not allowed!", address); } @Override @@ -43,7 +47,15 @@ public class RCONServerHandler extends ChannelInboundHandlerAdapter { } try { - byte[] d = new byte[data.readableBytes()]; + int readableBytes = data.readableBytes(); + int maxPayloadBytes = maxPayloadBytes(); + if (readableBytes > maxPayloadBytes) { + writeAndClose(ctx, "PAYLOAD_TOO_LARGE"); + LOGGER.warn("Rejected oversized RCON payload: {} bytes (max {})", readableBytes, maxPayloadBytes); + return; + } + + byte[] d = new byte[readableBytes]; data.getBytes(0, d); String message = new String(d, java.nio.charset.StandardCharsets.UTF_8); Gson gson = GSON; @@ -60,12 +72,34 @@ public class RCONServerHandler extends ChannelInboundHandlerAdapter { e.printStackTrace(); } - ChannelFuture f = ctx.channel().write(Unpooled.copiedBuffer(response.getBytes(java.nio.charset.StandardCharsets.UTF_8)), ctx.channel().voidPromise()); - ctx.channel().flush(); - ctx.flush(); - f.channel().close(); + writeAndClose(ctx, response); } finally { data.release(); } } + + static int maxPayloadBytes() { + if (Emulator.getConfig() == null) { + return DEFAULT_MAX_PAYLOAD_BYTES; + } + + int configured = Emulator.getConfig().getInt("rcon.max_payload_bytes", DEFAULT_MAX_PAYLOAD_BYTES); + return configured > 0 ? configured : DEFAULT_MAX_PAYLOAD_BYTES; + } + + static String remoteAddress(ChannelHandlerContext ctx) { + SocketAddress socketAddress = ctx.channel().remoteAddress(); + if (socketAddress instanceof InetSocketAddress inetSocketAddress && inetSocketAddress.getAddress() != null) { + return inetSocketAddress.getAddress().getHostAddress(); + } + + return socketAddress == null ? "" : socketAddress.toString().replace("/", ""); + } + + private static void writeAndClose(ChannelHandlerContext ctx, String response) { + ChannelFuture f = ctx.channel().write(Unpooled.copiedBuffer(response.getBytes(java.nio.charset.StandardCharsets.UTF_8)), ctx.channel().voidPromise()); + ctx.channel().flush(); + ctx.flush(); + f.channel().close(); + } } diff --git a/Emulator/src/test/java/com/eu/habbo/networking/rconserver/RCONServerHandlerContractTest.java b/Emulator/src/test/java/com/eu/habbo/networking/rconserver/RCONServerHandlerContractTest.java index 893d556b..54e6586a 100644 --- a/Emulator/src/test/java/com/eu/habbo/networking/rconserver/RCONServerHandlerContractTest.java +++ b/Emulator/src/test/java/com/eu/habbo/networking/rconserver/RCONServerHandlerContractTest.java @@ -50,6 +50,30 @@ class RCONServerHandlerContractTest { assertTrue(registerIndex < serverIndex, "RCON rate limit defaults must be registered before RCONServer is constructed"); } + @Test + void rconPayloadSizeIsBoundedBeforeBufferCopy() throws Exception { + String handler = handlerSource(); + String emulator = emulatorSource(); + + int readableBytes = handler.indexOf("int readableBytes = data.readableBytes()"); + int maxPayload = handler.indexOf("int maxPayloadBytes = maxPayloadBytes()", readableBytes); + int oversizedGuard = handler.indexOf("readableBytes > maxPayloadBytes", maxPayload); + int byteArray = handler.indexOf("new byte[readableBytes]", readableBytes); + + assertTrue(handler.contains("DEFAULT_MAX_PAYLOAD_BYTES = 64 * 1024"), + "RCON handler should have a conservative default payload cap"); + assertTrue(handler.contains("rcon.max_payload_bytes"), + "RCON max payload should be configurable"); + assertTrue(readableBytes > -1, "RCON handler must read ByteBuf size before allocation"); + assertTrue(maxPayload > readableBytes, "RCON handler must resolve max payload before allocation"); + assertTrue(oversizedGuard > maxPayload, "RCON handler must reject oversized payloads"); + assertTrue(oversizedGuard < byteArray, "Oversized RCON payloads must be rejected before byte array allocation"); + assertTrue(handler.contains("PAYLOAD_TOO_LARGE"), + "RCON callers need a deterministic response for oversized payloads"); + assertTrue(emulator.contains("register(\"rcon.max_payload_bytes\", \"65536\")"), + "RCON max payload default must be registered before startup"); + } + @Test void inboundByteBufIsReleasedFromFinallyBlock() throws Exception { String source = handlerSource(); @@ -59,4 +83,16 @@ class RCONServerHandlerContractTest { assertTrue(finallyIndex >= 0, "RCON channelRead must release inbound ByteBufs from a finally block"); assertTrue(releaseIndex > finallyIndex, "RCON channelRead must release the inbound ByteBuf after finally starts"); } + + @Test + void rconWhitelistUsesSocketAddressInsteadOfStringSplitting() throws Exception { + String source = handlerSource(); + + assertTrue(source.contains("InetSocketAddress"), + "RCON whitelist should resolve socket addresses instead of parsing remoteAddress.toString()"); + assertTrue(source.contains("getHostAddress()"), + "RCON whitelist should compare the resolved host address"); + assertTrue(!source.contains(".toString().split(\":\")"), + "RCON whitelist must not split host:port strings because that breaks IPv6 addresses"); + } }