Merge pull request #217 from simoleo89/fix/nitro-secure-asset-safety

fix(auth): bound secure asset file reads
This commit is contained in:
DuckieTM
2026-06-17 09:53:08 +02:00
committed by GitHub
2 changed files with 61 additions and 0 deletions
@@ -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('\\', '/');
@@ -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");
}
}