Server 2.10.4: HTTPS for the web panel and hostile client hardening
- Panel over HTTPS on the game port (TLS handshake detected like HTTP); self-signed certificate in data/panel_cert.pfx (fingerprint in the log) or an own PFX (PanelCertificatePath); remote HTTP is redirected to HTTPS (not locally / behind a proxy); cookie Secure over HTTPS; PanelHttps=false turns it off - Panel login: also locked per user name (16 failures / 15 min), failure lists bounded, user names in the log without control characters - panel_users.json, panel_sessions.json, server.json, certificate: owner-only on Linux - Players: names/chat without control or direction characters (no fake log lines), no '<' '>' in names; '* ' system-style chat, msco.* messages (time/weather) and MSCO_INSTALL only from admins; NaN/absurd money changes dropped (could break the shared money for good) - Extra headers: Permissions-Policy, Cross-Origin-Opener-Policy - 11 new tests (165 pass) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+64
-5
@@ -57,6 +57,8 @@ public sealed class GameServer
|
||||
// ---------------------------------------------------------------- Mods (data/mods)
|
||||
List<ServerMod> mods = new();
|
||||
const int ModPartBytes = 60_000, MaxModMessageBytes = 64 * 1024;
|
||||
/// <summary>Größte Geldänderung pro Nachricht (Einkäufe, Jobs) - mehr ist ein manipulierter Client.</summary>
|
||||
const float MaxMoneyDelta = 500_000f;
|
||||
|
||||
/// <summary>Mods aus data/mods (neu) laden. Neue Spieler bekommen die neue Liste; Verbundene erst beim nächsten Beitritt.</summary>
|
||||
public string ReloadMods()
|
||||
@@ -551,11 +553,31 @@ public sealed class GameServer
|
||||
{
|
||||
case MsgType.ObjStates: world.ApplyObjStates(m.Payload); break;
|
||||
case MsgType.ObjRelease: world.ApplyObjRelease(m.Payload); break;
|
||||
case MsgType.FsmEvent: world.AddFsmEvent(m.Payload); break;
|
||||
case MsgType.FsmEvent:
|
||||
{
|
||||
// Pfad, FSM, Event: "MSCO_INSTALL" (Teil per Admin-Menü einbauen) nur von Admins
|
||||
try
|
||||
{
|
||||
var fr = m.Reader();
|
||||
fr.ReadString(); fr.ReadString();
|
||||
if (fr.ReadString() == "MSCO_INSTALL" && !c.IsAdmin) return;
|
||||
}
|
||||
catch { return; }
|
||||
world.AddFsmEvent(m.Payload);
|
||||
break;
|
||||
}
|
||||
case MsgType.Player:
|
||||
if (DateTime.UtcNow >= c.TrackPositionFrom) world.ApplyPlayer(c.SteamId, m.Payload);
|
||||
break;
|
||||
case MsgType.MoneyDelta: world.ApplyMoney(m.Payload); break;
|
||||
case MsgType.MoneyDelta:
|
||||
{
|
||||
// Ungültige Werte (NaN/unendlich) würden die gemeinsame Kasse dauerhaft zerstören - nicht annehmen
|
||||
if (m.Payload.Length < 4) return;
|
||||
float delta = BitConverter.ToSingle(m.Payload, 0);
|
||||
if (!float.IsFinite(delta) || Math.Abs(delta) > MaxMoneyDelta) return;
|
||||
world.ApplyMoney(m.Payload);
|
||||
break;
|
||||
}
|
||||
case MsgType.DoorState: world.ApplyDoor(m.Payload); break;
|
||||
case MsgType.VarSync: world.ApplyVars(m.Payload); break;
|
||||
case MsgType.WorldVars: world.ApplyJobVars(m.Payload); break;
|
||||
@@ -578,11 +600,28 @@ public sealed class GameServer
|
||||
break;
|
||||
case MsgType.ModMessage:
|
||||
if (m.Payload.Length > MaxModMessageBytes) return;
|
||||
// "msco.*" sind Nachrichten von MSCO selbst (z.B. msco.world = Uhrzeit/Wetter vom Admin-Menü) - nur von Admins
|
||||
string modId;
|
||||
try { modId = m.Reader().ReadString(); } catch { return; }
|
||||
if (modId.StartsWith("msco.", StringComparison.OrdinalIgnoreCase) && !c.IsAdmin) return;
|
||||
break;
|
||||
case MsgType.Chat:
|
||||
{
|
||||
if (m.Payload.Length > 1024) return; // Spam/Unsinn nicht weiterleiten
|
||||
Log($"[chat] {c.Name}: {m.Reader().ReadString()}");
|
||||
string text;
|
||||
try { text = m.Reader().ReadString(); } catch { return; }
|
||||
// "* ..." zeigt das Spiel als Server-/Admin-Meldung - nur Admins dürfen so schreiben (sonst gefälschte
|
||||
// "XY was banned"-Meldungen); bei anderen wird es normaler Chat
|
||||
if (text.StartsWith("* ") && !c.IsAdmin)
|
||||
{
|
||||
text = text.TrimStart('*', ' ');
|
||||
if (text.Length == 0) return;
|
||||
string clean = text;
|
||||
m = new Message(m.Type, Wire.Build(w => w.Write(clean)));
|
||||
}
|
||||
Log($"[chat] {c.Name}: {CleanText(text, 300)}");
|
||||
break;
|
||||
}
|
||||
case MsgType.TrafficStates:
|
||||
if (c.Id != WorldAuthorityId) return; // nur einer simuliert den Verkehr
|
||||
break;
|
||||
@@ -607,8 +646,8 @@ public sealed class GameServer
|
||||
steamId = r.ReadUInt64();
|
||||
ticket = r.ReadBytes(r.ReadInt32());
|
||||
}
|
||||
c.Name = string.IsNullOrWhiteSpace(c.Name) ? "Player" : c.Name.Trim();
|
||||
if (c.Name.Length > 24) c.Name = c.Name[..24];
|
||||
c.Name = CleanText(c.Name ?? "", 24).Replace("<", "").Replace(">", "").Trim();
|
||||
if (c.Name.Length == 0) c.Name = "Player";
|
||||
// Spieler ohne Steam (Non-Steam-Server) werden über ihren Namen wiedererkannt (Position, Sperren)
|
||||
c.SteamId = steamId != 0 ? steamId.ToString() : "name:" + c.Name.ToLowerInvariant();
|
||||
|
||||
@@ -1199,6 +1238,26 @@ public sealed class GameServer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Text von einem Spieler (Name, Chat) bereinigen: keine Zeilenumbrüche/Steuerzeichen (keine gefälschten Zeilen in Log
|
||||
/// und Konsole), keine unsichtbaren Richtungs-Zeichen, gekürzt.
|
||||
/// </summary>
|
||||
public static string CleanText(string s, int max)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s)) return "";
|
||||
var sb = new System.Text.StringBuilder(Math.Min(s.Length, max));
|
||||
foreach (char ch in s)
|
||||
{
|
||||
if (sb.Length >= max) break;
|
||||
if (char.IsControl(ch)) { sb.Append(' '); continue; }
|
||||
// Richtungs-/Format-Zeichen (z.B. U+202E dreht Text um) weglassen
|
||||
var cat = char.GetUnicodeCategory(ch);
|
||||
if (cat == System.Globalization.UnicodeCategory.Format) continue;
|
||||
sb.Append(ch);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static void Log(string text)
|
||||
{
|
||||
string time = DateTime.Now.ToString("HH:mm:ss");
|
||||
|
||||
Reference in New Issue
Block a user