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);
|
}, 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));
|
ghostSessions.put(userId, new GhostSession(habbo, ssoTicket, future, previousEffectId, previousEffectEnd));
|
||||||
|
|
||||||
applyPausedEffect(habbo);
|
applyPausedEffect(habbo);
|
||||||
|
|||||||
+12
@@ -227,6 +227,18 @@ public final class WiredVariableReferenceSupport {
|
|||||||
USER_ASSIGNMENT_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(prefix));
|
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) {
|
public static SharedRoomAssignment getSharedRoomAssignment(WiredExtraVariableReference reference) {
|
||||||
if (reference == null || !reference.isRoomReference()) {
|
if (reference == null || !reference.isRoomReference()) {
|
||||||
return null;
|
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);
|
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.itemManager.clear();
|
||||||
|
|
||||||
this.unitManager.clearQueue();
|
this.unitManager.clearQueue();
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import java.sql.SQLException;
|
|||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
final class AuthHttpUtil {
|
public final class AuthHttpUtil {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(AuthHttpUtil.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(AuthHttpUtil.class);
|
||||||
|
|
||||||
@@ -132,7 +132,10 @@ final class AuthHttpUtil {
|
|||||||
String ipHeader = Emulator.getConfig() != null
|
String ipHeader = Emulator.getConfig() != null
|
||||||
? Emulator.getConfig().getValue("ws.ip.header", "")
|
? 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);
|
String hv = req.headers().get(ipHeader);
|
||||||
if (hv != null && !hv.isEmpty()) {
|
if (hv != null && !hv.isEmpty()) {
|
||||||
int comma = hv.indexOf(',');
|
int comma = hv.indexOf(',');
|
||||||
@@ -148,6 +151,32 @@ final class AuthHttpUtil {
|
|||||||
return "";
|
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) {
|
static boolean checkPassword(String plain, String stored) {
|
||||||
String compatible = stored.startsWith("$2y$") ? "$2a$" + stored.substring(4) : stored;
|
String compatible = stored.startsWith("$2y$") ? "$2a$" + stored.substring(4) : stored;
|
||||||
try {
|
try {
|
||||||
|
|||||||
+8
-2
@@ -2,6 +2,7 @@ package com.eu.habbo.networking.gameserver.handlers;
|
|||||||
|
|
||||||
import com.eu.habbo.Emulator;
|
import com.eu.habbo.Emulator;
|
||||||
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
import com.eu.habbo.networking.gameserver.GameServerAttributes;
|
||||||
|
import com.eu.habbo.networking.gameserver.auth.AuthHttpUtil;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
import io.netty.channel.ChannelFutureListener;
|
import io.netty.channel.ChannelFutureListener;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
@@ -65,9 +66,14 @@ public class WebSocketHttpHandler extends ChannelInboundHandlerAdapter {
|
|||||||
|
|
||||||
private static void captureForwardedIp(ChannelHandlerContext ctx, HttpMessage req) {
|
private static void captureForwardedIp(ChannelHandlerContext ctx, HttpMessage req) {
|
||||||
String ipHeader = Emulator.getConfig().getValue("ws.ip.header", "");
|
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);
|
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