You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
fix: trusted-proxy gate for forwarded IP, wired-var cache + ghost-session cleanup
Security (S3): - AuthHttpUtil/WebSocketHttpHandler: only honour the configured ws.ip.header forwarded-IP header when the DIRECT peer is a trusted reverse proxy, instead of trusting it unconditionally. Loopback is always trusted; extra proxies can be allow-listed (exact IP or string prefix, comma-separated) via the new `ws.ip.header.trusted` config key — default-deny so the header can't be spoofed from the open internet to evade per-IP rate limiting and IP bans. Also take only the first comma token when setting the game-session WS_IP. Leak cleanup (C4): - WiredVariableReferenceSupport.invalidateRoom(): drop a room's shared wired-variable assignment caches; called from Room.dispose so the static USER/ROOM_ASSIGNMENT_CACHE maps don't retain entries for the JVM lifetime. - SessionResumeManager.parkHabbo: if the scheduler refuses the grace-expiry task (future == null), disconnect immediately instead of parking an un-reapable GhostSession that would pin the Habbo + room refs forever. Note: ws.ip.header.trusted defaults to loopback-only; deployments whose proxy is on another host must add its IP/prefix to that key or client IPs will collapse to the proxy address.
This commit is contained in:
@@ -71,6 +71,15 @@ public class SessionResumeManager {
|
||||
}
|
||||
}, graceSeconds * 1000);
|
||||
|
||||
if (future == null) {
|
||||
// The scheduler refused the grace-expiry task (pool saturated or
|
||||
// shutting down). Parking now would leave a GhostSession that nothing
|
||||
// can ever reap (the Habbo + room refs pinned for the JVM lifetime),
|
||||
// so disconnect immediately instead.
|
||||
performFullDisconnect(habbo);
|
||||
return false;
|
||||
}
|
||||
|
||||
ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future, previousEffectId, previousEffectEnd));
|
||||
|
||||
applyPausedEffect(habbo);
|
||||
|
||||
+12
@@ -227,6 +227,18 @@ public final class WiredVariableReferenceSupport {
|
||||
USER_ASSIGNMENT_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(prefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops all cached shared-variable assignments belonging to a room. Both
|
||||
* caches are keyed "roomId:itemId[:userId]", so the trailing colon makes the
|
||||
* prefix match the exact room id. Called on room dispose so the static caches
|
||||
* don't retain entries for the JVM lifetime.
|
||||
*/
|
||||
public static void invalidateRoom(int roomId) {
|
||||
String prefix = roomId + ":";
|
||||
USER_ASSIGNMENT_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(prefix));
|
||||
ROOM_ASSIGNMENT_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(prefix));
|
||||
}
|
||||
|
||||
public static SharedRoomAssignment getSharedRoomAssignment(WiredExtraVariableReference reference) {
|
||||
if (reference == null || !reference.isRoomReference()) {
|
||||
return null;
|
||||
|
||||
@@ -1028,6 +1028,10 @@ public class Room implements Comparable<Room>, ISerialize, Runnable {
|
||||
com.eu.habbo.habbohotel.wired.core.WiredManager.getEngine().clearRoomDiagnostics(this.id);
|
||||
}
|
||||
|
||||
// Drop this room's shared wired-variable assignment caches (otherwise
|
||||
// they accrue per (room, item, user) for the JVM lifetime).
|
||||
com.eu.habbo.habbohotel.items.interactions.wired.extra.WiredVariableReferenceSupport.invalidateRoom(this.id);
|
||||
|
||||
this.itemManager.clear();
|
||||
|
||||
this.unitManager.clearQueue();
|
||||
|
||||
@@ -28,7 +28,7 @@ import java.sql.SQLException;
|
||||
import java.util.Base64;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
final class AuthHttpUtil {
|
||||
public final class AuthHttpUtil {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AuthHttpUtil.class);
|
||||
|
||||
@@ -132,7 +132,10 @@ final class AuthHttpUtil {
|
||||
String ipHeader = Emulator.getConfig() != null
|
||||
? Emulator.getConfig().getValue("ws.ip.header", "")
|
||||
: "";
|
||||
if (!ipHeader.isEmpty() && req.headers().contains(ipHeader)) {
|
||||
// Only trust a client-supplied forwarded-IP header when the DIRECT peer
|
||||
// is a trusted reverse proxy; otherwise an attacker hitting the port
|
||||
// directly could spoof it to evade per-IP rate limiting and IP bans.
|
||||
if (!ipHeader.isEmpty() && req.headers().contains(ipHeader) && isTrustedProxy(ctx)) {
|
||||
String hv = req.headers().get(ipHeader);
|
||||
if (hv != null && !hv.isEmpty()) {
|
||||
int comma = hv.indexOf(',');
|
||||
@@ -148,6 +151,32 @@ final class AuthHttpUtil {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the channel's direct peer may set a forwarded-IP header. Loopback
|
||||
* is always trusted; additional proxies can be allow-listed (exact IP or
|
||||
* string prefix, comma-separated) via the {@code ws.ip.header.trusted}
|
||||
* config key. Default-deny so the header can't be spoofed from the open net.
|
||||
*/
|
||||
public static boolean isTrustedProxy(ChannelHandlerContext ctx) {
|
||||
String peerIp = (ctx.channel().remoteAddress() instanceof InetSocketAddress a)
|
||||
? a.getAddress().getHostAddress() : null;
|
||||
if (peerIp == null || peerIp.isEmpty()) return false;
|
||||
if (peerIp.equals("127.0.0.1") || peerIp.equals("::1") || peerIp.equals("0:0:0:0:0:0:0:1")) {
|
||||
return true;
|
||||
}
|
||||
String trusted = Emulator.getConfig() != null
|
||||
? Emulator.getConfig().getValue("ws.ip.header.trusted", "")
|
||||
: "";
|
||||
if (trusted.isEmpty()) return false;
|
||||
for (String entry : trusted.split(",")) {
|
||||
String prefix = entry.trim();
|
||||
if (!prefix.isEmpty() && (peerIp.equals(prefix) || peerIp.startsWith(prefix))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean checkPassword(String plain, String stored) {
|
||||
String compatible = stored.startsWith("$2y$") ? "$2a$" + stored.substring(4) : stored;
|
||||
try {
|
||||
|
||||
+8
-2
@@ -2,6 +2,7 @@ package com.eu.habbo.networking.gameserver.handlers;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
||||
import com.eu.habbo.networking.gameserver.auth.AuthHttpUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
@@ -65,9 +66,14 @@ public class WebSocketHttpHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private static void captureForwardedIp(ChannelHandlerContext ctx, HttpMessage req) {
|
||||
String ipHeader = Emulator.getConfig().getValue("ws.ip.header", "");
|
||||
if (!ipHeader.isEmpty() && req.headers().contains(ipHeader)) {
|
||||
// Only honour the forwarded-IP header from a trusted reverse proxy,
|
||||
// otherwise the game-session IP (used for bans/rate-limits) is spoofable.
|
||||
if (!ipHeader.isEmpty() && req.headers().contains(ipHeader) && AuthHttpUtil.isTrustedProxy(ctx)) {
|
||||
String ip = req.headers().get(ipHeader);
|
||||
ctx.channel().attr(GameServerAttributes.WS_IP).set(ip);
|
||||
if (ip != null && !ip.isEmpty()) {
|
||||
int comma = ip.indexOf(',');
|
||||
ctx.channel().attr(GameServerAttributes.WS_IP).set((comma > 0 ? ip.substring(0, comma) : ip).trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user