Merge pull request #216 from simoleo89/fix/nitro-secure-api-safety

fix(auth): bound secure api payloads
This commit is contained in:
DuckieTM
2026-06-17 09:45:55 +02:00
committed by GitHub
2 changed files with 63 additions and 1 deletions
@@ -24,7 +24,9 @@ import java.util.concurrent.ConcurrentHashMap;
public class NitroSecureApiHandler extends ChannelDuplexHandler { public class NitroSecureApiHandler extends ChannelDuplexHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(NitroSecureApiHandler.class); private static final Logger LOGGER = LoggerFactory.getLogger(NitroSecureApiHandler.class);
private static final String ENABLED_CONFIG = "nitro.secure.api.enabled"; 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 String API_PREFIX = "/api/";
private static final int DEFAULT_MAX_PAYLOAD_BYTES = 64 * 1024;
private static final AttributeKey<Deque<SecureApiContext>> SECURE_CONTEXTS = private static final AttributeKey<Deque<SecureApiContext>> SECURE_CONTEXTS =
AttributeKey.valueOf("nitroSecureApiContexts"); AttributeKey.valueOf("nitroSecureApiContexts");
private static final ConcurrentHashMap<String, Long> NONCE_CACHE = new ConcurrentHashMap<>(); private static final ConcurrentHashMap<String, Long> NONCE_CACHE = new ConcurrentHashMap<>();
@@ -81,7 +83,14 @@ public class NitroSecureApiHandler extends ChannelDuplexHandler {
return; 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); req.content().getBytes(req.content().readerIndex(), encrypted);
byte[] clear = NitroSecureAssetHandler.decrypt(sessionKey, NitroSecureAssetHandler.fromHex(new String(encrypted, StandardCharsets.UTF_8))); byte[] clear = NitroSecureAssetHandler.decrypt(sessionKey, NitroSecureAssetHandler.fromHex(new String(encrypted, StandardCharsets.UTF_8)));
clear = unwrapEnvelope(clear, req, secureContext); clear = unwrapEnvelope(clear, req, secureContext);
@@ -173,6 +182,15 @@ public class NitroSecureApiHandler extends ChannelDuplexHandler {
return com.eu.habbo.Emulator.getConfig().getBoolean(ENABLED_CONFIG, true); 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) { private static byte[] unwrapEnvelope(byte[] clear, FullHttpRequest req, SecureApiContext secureContext) {
if (!requiresReplayEnvelope(req.method())) return clear; if (!requiresReplayEnvelope(req.method())) return clear;
@@ -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");
}
}