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:
@@ -129,6 +129,26 @@ try
|
||||
var longChat = await bob.WaitFor(MsgType.Chat, 800);
|
||||
Check(longChat == null, "Huge chat messages are not relayed");
|
||||
|
||||
Console.WriteLine("== Hostile clients");
|
||||
alice.Send(MsgType.Chat, Wire.Build(w => w.Write("* Bob was banned by an admin")));
|
||||
var fakeSystem = await bob.WaitFor(MsgType.Chat);
|
||||
Check(fakeSystem != null && ReadStringAfterSender(fakeSystem) == "Bob was banned by an admin",
|
||||
"Non-admins cannot send '* ' messages that look like server/admin messages");
|
||||
alice.Send(MsgType.ModMessage, Wire.Build(w => { w.Write("msco.world"); w.Write(17); w.Write(new byte[17]); }));
|
||||
Check(await bob.WaitFor(MsgType.ModMessage, 800) == null, "Reserved msco.* messages (time/weather) from non-admins are dropped");
|
||||
alice.Send(MsgType.ModMessage, Wire.Build(w => { w.Write("somemod"); w.Write(3); w.Write(new byte[] { 1, 2, 3 }); }));
|
||||
Check(await bob.WaitFor(MsgType.ModMessage, 2000) != null, "Normal mod messages are still relayed");
|
||||
alice.Send(MsgType.MoneyDelta, BitConverter.GetBytes(float.NaN));
|
||||
Check(await bob.WaitFor(MsgType.MoneyDelta, 800) == null, "NaN money changes are dropped (would break the shared money)");
|
||||
alice.Send(MsgType.MoneyDelta, BitConverter.GetBytes(1e9f));
|
||||
Check(await bob.WaitFor(MsgType.MoneyDelta, 800) == null, "Absurd money changes are dropped");
|
||||
alice.Send(MsgType.MoneyDelta, BitConverter.GetBytes(25.5f));
|
||||
Check(await bob.WaitFor(MsgType.MoneyDelta, 2000) != null, "Normal money changes are relayed");
|
||||
alice.Send(MsgType.FsmEvent, Wire.Build(w => { w.Write("SATSUMA|x"); w.Write("Assembly"); w.Write("MSCO_INSTALL"); w.Write(""); w.Write(0f); w.Write(0f); w.Write(0f); }));
|
||||
Check(await bob.WaitFor(MsgType.FsmEvent, 800) == null, "Admin part installs (MSCO_INSTALL) from non-admins are dropped");
|
||||
Check(GameServer.CleanText("Eve\n[12:00] Server: you are banned\u202E", 64) == "Eve [12:00] Server: you are banned",
|
||||
"Names/chat lose line breaks and invisible direction characters (no fake log lines)");
|
||||
|
||||
Console.WriteLine("== Bought / consumed items");
|
||||
alice.Send(MsgType.ItemAppeared, Wire.Build(w => { w.Write(4242); w.Write("beer case(itemx)"); w.Write(1f); w.Write(2f); w.Write(3f); w.Write(0f); w.Write(0f); w.Write(0f); w.Write(1f); }));
|
||||
Check(await bob.WaitFor(MsgType.ItemAppeared) != null, "Bought item is relayed to the other players");
|
||||
@@ -419,6 +439,29 @@ async Task PanelTests()
|
||||
Check(File.Exists(Path.Combine(dir, "panel_users.json")) && !File.ReadAllText(Path.Combine(dir, "panel_users.json")).Contains("supersecret1"),
|
||||
"Users are stored with a password hash, not the password");
|
||||
|
||||
// HTTPS auf dem Spiel-Port (selbst signiertes Zertifikat)
|
||||
System.Security.Cryptography.X509Certificates.X509Certificate2 seenCert = null;
|
||||
var tlsHandler = new HttpClientHandler
|
||||
{
|
||||
CookieContainer = new System.Net.CookieContainer(), UseCookies = true,
|
||||
ServerCertificateCustomValidationCallback = (_, cert, _, _) => { seenCert = cert == null ? null : new System.Security.Cryptography.X509Certificates.X509Certificate2(cert); return true; }
|
||||
};
|
||||
var tlsBrowser = new HttpClient(tlsHandler) { BaseAddress = new Uri($"https://127.0.0.1:{config.Port}/"), Timeout = TimeSpan.FromSeconds(10) };
|
||||
var tlsPage = await tlsBrowser.GetAsync("");
|
||||
Check(tlsPage.IsSuccessStatusCode && (await tlsPage.Content.ReadAsStringAsync()).Contains("MSCO Server Panel"),
|
||||
"The panel also works over HTTPS on the game port");
|
||||
Check(seenCert != null && host.Panel.CertificateFingerprint != null && File.Exists(Path.Combine(dir, "panel_cert.pfx"))
|
||||
&& host.Panel.CertificateFingerprint.Replace(":", "") == Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(seenCert.RawData)),
|
||||
"A self-signed certificate is created and its fingerprint is shown");
|
||||
var tlsLogin = new HttpRequestMessage(HttpMethod.Post, "api/login")
|
||||
{
|
||||
Content = new StringContent(System.Text.Json.JsonSerializer.Serialize(new { username = "owner", password = "supersecret1" }), System.Text.Encoding.UTF8, "application/json")
|
||||
};
|
||||
tlsLogin.Headers.Add("X-MSCO-Panel", "1");
|
||||
var tlsLoginRes = await tlsBrowser.SendAsync(tlsLogin);
|
||||
Check(tlsLoginRes.IsSuccessStatusCode && tlsLoginRes.Headers.TryGetValues("Set-Cookie", out var tlsCookies)
|
||||
&& tlsCookies.Any(v => v.Contains("secure", StringComparison.OrdinalIgnoreCase)), "Over HTTPS the login cookie is marked Secure");
|
||||
|
||||
// Spiel läuft auf demselben Port weiter
|
||||
var gamer = await TestClient.Join("PanelTarget", 0, null);
|
||||
Check(gamer.Welcome != null, "Game clients still join on the same port");
|
||||
|
||||
Reference in New Issue
Block a user