feat: dedicated server security hardening

Comprehensive security system to protect against packet-sniffing attacks,
XUID harvesting, privilege escalation, bot flooding, and XUID impersonation.

- Stream cipher: per-session XOR cipher with 4-message handshake via
  CustomPayloadPacket (MC|CKey, MC|CAck, MC|COn). Negotiated per-connection,
  backwards compatible (old clients/servers fall back to plaintext).
- Security gate: buffers all game data until cipher handshake completes,
  preventing unsecured clients from receiving any XUIDs or game state.
- Cipher handshake enforcer: kicks clients that don't complete the handshake
  within 5 seconds (configurable via require-secure-client).
- Identity tokens: persistent per-XUID tokens in identity-tokens.json,
  issued over the encrypted channel, verified on reconnect. Prevents XUID
  replay attacks. Client stores server-specific tokens.
- PROXY protocol v1: parses real client IPs from playit.gg tunnel headers
  so rate limiting, IP bans, and XUID spoof detection work per-player.
- Rate limiting: per-IP sliding window (default 5 connections/30s) with
  pending connection cap (default 10).
- Privilege hardening: OP requires ops.json, live checks on every command
  and privilege packet. Host-only server settings changes.
- XUID stripping: PreLoginPacket response sends INVALID_XUID placeholders.
- Packet validation: readUtf global string cap, reduced max packet size,
  stream desync protection on oversized strings.
- OpManager: persistent ops.json with XUID-based OP list.
- Whitelist improvements: whitelist add accepts player names with ambiguity
  detection, XUID cache from login attempts.
- revoketoken command: revoke identity tokens for players who lost theirs.
- server.log: persistent log file written alongside console output with
  flush-per-write to survive crashes.
- CLI security logging: consolidated per-join security summary with cipher
  status, token status, XUID, and real IP. Security warnings for kicks,
  spoofing, and unauthorized commands.
This commit is contained in:
itsRevela
2026-03-28 19:18:06 -05:00
parent ed3fffcc6a
commit ba3ebe666c
42 changed files with 3293 additions and 34 deletions

View File

@@ -17,6 +17,7 @@
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\Minecraft.Server\ServerLogManager.h"
#include "..\Minecraft.Server\Access\Access.h"
#include "..\Minecraft.Server\Security\SecurityConfig.h"
#include "..\Minecraft.World\Socket.h"
#endif
// #ifdef __PS3__
@@ -150,6 +151,20 @@ void PendingConnection::sendPreLoginResponse()
}
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Security: strip real XUIDs from pre-login response to prevent unauthenticated enumeration.
// The client receives the correct player count but cannot identify who is connected.
// Real XUID data is sent post-login via PlayerInfoPacket broadcasts.
if (ServerRuntime::Security::GetSettings().hidePlayerListPreLogin)
{
for (DWORD i = 0; i < ugcXuidCount; ++i)
{
ugcXuids[i] = INVALID_XUID;
}
ugcFriendsOnlyBits = 0;
}
#endif
#if 0
if (false)// server->onlineMode) // 4J - removed
{
@@ -203,6 +218,56 @@ void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet)
duplicateXuid = true;
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Cross-reference: if someone claims the same XUID as an existing player from a different IP,
// log and reject as a potential spoofing attempt.
// Note: this runs on the main tick thread (via PendingConnection::tick -> Connection::tick ->
// handleLogin), same thread that mutates the player list, so no lock is needed.
if (!duplicateXuid && loginXuid != INVALID_XUID)
{
std::string newIp;
unsigned char newSmallId = GetPendingConnectionSmallId(connection);
bool hasNewIp = ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(newSmallId, &newIp);
for (auto &existingPlayer : server->getPlayers()->players)
{
if (existingPlayer == nullptr) continue;
PlayerUID existingXuid = existingPlayer->connection->m_offlineXUID;
if (existingXuid == INVALID_XUID) existingXuid = existingPlayer->connection->m_onlineXUID;
if (existingXuid == loginXuid)
{
if (hasNewIp)
{
std::string existingIp;
INetworkPlayer *np = existingPlayer->connection->getNetworkPlayer();
if (np != nullptr)
{
unsigned char existingSmallId = np->GetSmallId();
if (ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(existingSmallId, &existingIp))
{
if (existingIp != newIp)
{
app.DebugPrintf("SECURITY: XUID spoofing suspected - XUID 0x%016llx claimed from IP %s while already connected from IP %s\n",
(unsigned long long)loginXuid, newIp.c_str(), existingIp.c_str());
ServerRuntime::ServerLogManager::OnXuidSpoofDetected(newSmallId, name, newIp.c_str(), existingIp.c_str());
duplicateXuid = true;
}
}
}
}
else
{
// Cannot verify IP -- treat same-XUID connection as suspicious
app.DebugPrintf("SECURITY: XUID 0x%016llx claimed but could not verify source IP\n",
(unsigned long long)loginXuid);
duplicateXuid = true;
}
break;
}
}
}
#endif
bool bannedXuid = false;
if (loginXuid != INVALID_XUID)
{
@@ -243,7 +308,11 @@ void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet)
else if (!whitelistSatisfied)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Cache name->XUID so `whitelist add <name>` can resolve the XUID
ServerRuntime::ServerLogManager::CachePlayerXuid(name, loginXuid);
ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_NotWhitelisted);
app.DebugPrintf("WHITELIST: Rejected %ls (XUID: 0x%016llx) - use 'whitelist add %ls' to allow\n",
name.c_str(), (unsigned long long)loginXuid, name.c_str());
#endif
disconnect(DisconnectPacket::eDisconnect_Banned);
}
@@ -330,11 +399,17 @@ void PendingConnection::handleAcceptedLogin(shared_ptr<LoginPacket> packet)
PlayerUID playerXuid = packet->m_offlineXuid;
if(playerXuid == INVALID_XUID) playerXuid = packet->m_onlineXuid;
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
// Cache name->XUID for console commands (whitelist add, revoketoken, etc.)
ServerRuntime::ServerLogManager::CachePlayerXuid(name, playerXuid);
#endif
shared_ptr<ServerPlayer> playerEntity = server->getPlayers()->getPlayerForLogin(this, name, playerXuid,packet->m_onlineXuid);
if (playerEntity != nullptr)
{
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnAcceptedPlayerLogin(GetPendingConnectionSmallId(connection), name);
ServerRuntime::ServerLogManager::OnAcceptedPlayerLogin(GetPendingConnectionSmallId(connection), name,
packet->m_offlineXuid, packet->m_onlineXuid, packet->m_isGuest);
#endif
server->getPlayers()->placeNewPlayer(connection, playerEntity, packet);
connection = nullptr; // We've moved responsibility for this over to the new PlayerConnection, nullptr so we don't delete our reference to it here in our dtor