diff --git a/Emulator/src/main/java/com/eu/habbo/Emulator.java b/Emulator/src/main/java/com/eu/habbo/Emulator.java index 52c7f8b1..a9f70bd7 100644 --- a/Emulator/src/main/java/com/eu/habbo/Emulator.java +++ b/Emulator/src/main/java/com/eu/habbo/Emulator.java @@ -166,6 +166,8 @@ 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.config.max_file_bytes", "2097152"); + Emulator.config.register("nitro.secure.gamedata.max_file_bytes", "16777216"); 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/NitroSecureAssetHandler.java b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java index 851cf90b..645408da 100644 --- a/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java +++ b/Emulator/src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java @@ -35,6 +35,8 @@ public class NitroSecureAssetHandler extends ChannelInboundHandlerAdapter { private static final String BOOTSTRAP_PATH = "/nitro-sec/bootstrap"; private static final String FILE_PATH = "/nitro-sec/file"; private static final int MAX_BOOTSTRAP_BODY_BYTES = 4096; + private static final int DEFAULT_MAX_CONFIG_BYTES = 2 * 1024 * 1024; + private static final int DEFAULT_MAX_GAMEDATA_BYTES = 16 * 1024 * 1024; private static final SecureRandom RNG = new SecureRandom(); private static final KeyPair SERVER_KEYPAIR = createServerKeyPair(); private static final String SERVER_KEY_FINGERPRINT = fingerprint(SERVER_KEYPAIR.getPublic().getEncoded()); @@ -146,6 +148,9 @@ public class NitroSecureAssetHandler extends ChannelInboundHandlerAdapter { if (!target.startsWith(root)) throw new IllegalArgumentException("Invalid file."); if (!Files.isRegularFile(target)) throw new IOException("Not found"); + long size = Files.size(target); + int maxBytes = maxAssetBytes(kind); + if (size > maxBytes) throw new IllegalArgumentException("File too large."); String cacheKey = kind + ":" + target; long modified = Files.getLastModifiedTime(target).toMillis(); @@ -158,6 +163,14 @@ public class NitroSecureAssetHandler extends ChannelInboundHandlerAdapter { return bytes; } + static int maxAssetBytes(String kind) { + boolean config = "config".equals(kind); + String key = config ? "nitro.secure.config.max_file_bytes" : "nitro.secure.gamedata.max_file_bytes"; + int fallback = config ? DEFAULT_MAX_CONFIG_BYTES : DEFAULT_MAX_GAMEDATA_BYTES; + int configured = Emulator.getConfig().getInt(key, fallback); + return configured > 0 ? configured : fallback; + } + private static String normalizeFile(String file) { if (file == null) throw new IllegalArgumentException("Missing file."); String value = URLDecoder.decode(file, StandardCharsets.UTF_8).replace('\\', '/'); diff --git a/Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandlerContractTest.java b/Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandlerContractTest.java new file mode 100644 index 00000000..5bc8465e --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandlerContractTest.java @@ -0,0 +1,48 @@ +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 NitroSecureAssetHandlerContractTest { + private static String handlerSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/networking/gameserver/auth/NitroSecureAssetHandler.java")); + } + + private static String emulatorSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/Emulator.java")); + } + + @Test + void secureAssetFilesAreSizeCheckedBeforeReadAndCache() throws Exception { + String handler = handlerSource(); + String emulator = emulatorSource(); + + int size = handler.indexOf("long size = Files.size(target)"); + int maxBytes = handler.indexOf("int maxBytes = maxAssetBytes(kind)", size); + int oversizedGuard = handler.indexOf("size > maxBytes", maxBytes); + int cacheLookup = handler.indexOf("CACHE.get(cacheKey)", oversizedGuard); + int readAllBytes = handler.indexOf("Files.readAllBytes(target)", oversizedGuard); + + assertTrue(handler.contains("DEFAULT_MAX_CONFIG_BYTES = 2 * 1024 * 1024"), + "Secure config assets should have a conservative default file cap"); + assertTrue(handler.contains("DEFAULT_MAX_GAMEDATA_BYTES = 16 * 1024 * 1024"), + "Secure gamedata assets should have a bounded default file cap"); + assertTrue(handler.contains("nitro.secure.config.max_file_bytes"), + "Secure config max file size should be configurable"); + assertTrue(handler.contains("nitro.secure.gamedata.max_file_bytes"), + "Secure gamedata max file size should be configurable"); + assertTrue(size > -1, "Secure assets must inspect file size before loading bytes"); + assertTrue(maxBytes > size, "Secure assets must resolve the configured cap before loading bytes"); + assertTrue(oversizedGuard > maxBytes, "Secure assets must reject oversized files"); + assertTrue(oversizedGuard < cacheLookup, "Oversized secure assets must not be served from cache"); + assertTrue(oversizedGuard < readAllBytes, "Oversized secure assets must be rejected before readAllBytes"); + assertTrue(emulator.contains("register(\"nitro.secure.config.max_file_bytes\", \"2097152\")"), + "Secure config max file size default must be registered before startup"); + assertTrue(emulator.contains("register(\"nitro.secure.gamedata.max_file_bytes\", \"16777216\")"), + "Secure gamedata max file size default must be registered before startup"); + } +}