From 547c5ef157769e915189db99944a392e29c6b64e Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 16 Jun 2026 20:08:42 +0200 Subject: [PATCH] fix(auth): bound secure api payloads --- .../src/main/java/com/eu/habbo/Emulator.java | 1 + .../auth/NitroSecureApiHandler.java | 20 ++++++++- .../NitroSecureApiHandlerContractTest.java | 44 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandlerContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/Emulator.java b/Emulator/src/main/java/com/eu/habbo/Emulator.java index 52c7f8b1..d776d283 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("nitro.secure.api.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/gameserver/auth/NitroSecureApiHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java index 45528857..afe09e5c 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java @@ -24,7 +24,9 @@ import java.util.concurrent.ConcurrentHashMap; public class NitroSecureApiHandler extends ChannelDuplexHandler { private static final Logger LOGGER = LoggerFactory.getLogger(NitroSecureApiHandler.class); private static final String ENABLED_CONFIG = "nitro.secure.api.enabled"; + private static final String MAX_PAYLOAD_CONFIG = "nitro.secure.api.max_payload_bytes"; private static final String API_PREFIX = "/api/"; + private static final int DEFAULT_MAX_PAYLOAD_BYTES = 64 * 1024; private static final AttributeKey> SECURE_CONTEXTS = AttributeKey.valueOf("nitroSecureApiContexts"); private static final ConcurrentHashMap NONCE_CACHE = new ConcurrentHashMap<>(); @@ -81,7 +83,14 @@ public class NitroSecureApiHandler extends ChannelDuplexHandler { return; } - byte[] encrypted = new byte[req.content().readableBytes()]; + int readableBytes = req.content().readableBytes(); + int maxPayloadBytes = maxPayloadBytes(); + if (readableBytes > maxPayloadBytes) { + sendText(ctx, req, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE, "Secure payload too large."); + return; + } + + byte[] encrypted = new byte[readableBytes]; req.content().getBytes(req.content().readerIndex(), encrypted); byte[] clear = NitroSecureAssetHandler.decrypt(sessionKey, NitroSecureAssetHandler.fromHex(new String(encrypted, StandardCharsets.UTF_8))); clear = unwrapEnvelope(clear, req, secureContext); @@ -173,6 +182,15 @@ public class NitroSecureApiHandler extends ChannelDuplexHandler { return com.eu.habbo.Emulator.getConfig().getBoolean(ENABLED_CONFIG, true); } + static int maxPayloadBytes() { + if (com.eu.habbo.Emulator.getConfig() == null) { + return DEFAULT_MAX_PAYLOAD_BYTES; + } + + int configured = com.eu.habbo.Emulator.getConfig().getInt(MAX_PAYLOAD_CONFIG, DEFAULT_MAX_PAYLOAD_BYTES); + return configured > 0 ? configured : DEFAULT_MAX_PAYLOAD_BYTES; + } + private static byte[] unwrapEnvelope(byte[] clear, FullHttpRequest req, SecureApiContext secureContext) { if (!requiresReplayEnvelope(req.method())) return clear; diff --git a/Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandlerContractTest.java b/Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandlerContractTest.java new file mode 100644 index 00000000..0e708197 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandlerContractTest.java @@ -0,0 +1,44 @@ +package com.eu.habbo.networking.gameserver.auth; + +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 NitroSecureApiHandlerContractTest { + private static String handlerSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureApiHandler.java")); + } + + private static String emulatorSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/Emulator.java")); + } + + @Test + void encryptedApiPayloadSizeIsBoundedBeforeCopyAndDecrypt() throws Exception { + String handler = handlerSource(); + String emulator = emulatorSource(); + + int readableBytes = handler.indexOf("int readableBytes = req.content().readableBytes()"); + int maxPayload = handler.indexOf("int maxPayloadBytes = maxPayloadBytes()", readableBytes); + int oversizedGuard = handler.indexOf("readableBytes > maxPayloadBytes", maxPayload); + int byteArray = handler.indexOf("new byte[readableBytes]", readableBytes); + int decrypt = handler.indexOf("NitroSecureAssetHandler.decrypt", byteArray); + + assertTrue(handler.contains("DEFAULT_MAX_PAYLOAD_BYTES = 64 * 1024"), + "Secure API handler should have a conservative default payload cap"); + assertTrue(handler.contains("nitro.secure.api.max_payload_bytes"), + "Secure API max payload should be configurable"); + assertTrue(readableBytes > -1, "Secure API handler must read content size before allocation"); + assertTrue(maxPayload > readableBytes, "Secure API handler must resolve max payload before allocation"); + assertTrue(oversizedGuard > maxPayload, "Secure API handler must reject oversized encrypted payloads"); + assertTrue(oversizedGuard < byteArray, "Oversized encrypted payloads must be rejected before byte array allocation"); + assertTrue(byteArray < decrypt, "Secure API payload must be bounded before decrypting"); + assertTrue(handler.contains("REQUEST_ENTITY_TOO_LARGE"), + "Secure API callers need a deterministic status for oversized encrypted payloads"); + assertTrue(emulator.contains("register(\"nitro.secure.api.max_payload_bytes\", \"65536\")"), + "Secure API max payload default must be registered before startup"); + } +}