From 56e8e2554c1d093dcc8af071b6ff5290a6bbc12f Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:44:12 +0100 Subject: [PATCH] fix(multiplayer): validate allocated config ids --- Game/scripts/server_config.gd | 11 +++++++++++ Game/tests/cases/test_server_config.gd | 7 +++++++ multiplayer-next.md | 2 ++ 3 files changed, 20 insertions(+) diff --git a/Game/scripts/server_config.gd b/Game/scripts/server_config.gd index dec25f83..7f1e9336 100644 --- a/Game/scripts/server_config.gd +++ b/Game/scripts/server_config.gd @@ -276,6 +276,10 @@ func _validate() -> void: for key in ["match-id", "server-id", "playlist-version", "playlist", "client-build", "assignment-expiry-unix", "server-image-digest", "transport", "region"]: if str(values[key]).is_empty(): errors.append("--allocated-mode requires --%s" % key) + if not _is_opaque_id(String(values["match-id"])): + errors.append("--match-id must be an opaque ID of 16-128 safe characters") + if not _is_opaque_id(String(values["server-id"])): + errors.append("--server-id must be an opaque ID of 16-128 safe characters") if int(values["assignment-expiry-unix"]) <= int(Time.get_unix_time_from_system()): errors.append("--assignment-expiry-unix must be in the future") if String(values["join-authorisations-file"]).is_empty(): @@ -307,6 +311,13 @@ static func _is_sha256_digest(value: String) -> bool: return true +static func _is_opaque_id(value: String) -> bool: + if value.length() < 16 or value.length() > 128: + return false + var resource_pattern := RegEx.create_from_string("^[A-Za-z0-9_-]+$") + return resource_pattern.search(value) != null + + static func _kind_name(kind: int) -> String: match kind: Kind.BOOL: return "bool" diff --git a/Game/tests/cases/test_server_config.gd b/Game/tests/cases/test_server_config.gd index 11487ce9..85c797c3 100644 --- a/Game/tests/cases/test_server_config.gd +++ b/Game/tests/cases/test_server_config.gd @@ -152,6 +152,13 @@ func test_allocated_mode_rejects_invalid_transport_region_or_digest() -> void: ] var config = _parse(args) assert_true(not config.is_valid(), "invalid compatibility values are rejected") + var unsafe_id = _parse([ + "--allocated-mode", "--match-id=short", "--server-id=server/unsafe", "--playlist-version=v", + "--client-build=client", "--assignment-expiry-unix=%d" % (Time.get_unix_time_from_system() + 3600), + "--server-image-digest=sha256:" + "a".repeat(64), "--playlist=casual", "--transport=enet", "--region=EU", + "--join-authorisations-file=/run/secrets/join-authorisations.json", "--join-authorisations-key-file=/run/secrets/join-authorisations.key" + ]) + assert_true(not unsafe_id.is_valid(), "short or unsafe allocated identifiers are rejected") func test_allocated_mode_rejects_missing_or_expired_assignment_manifest_fields() -> void: diff --git a/multiplayer-next.md b/multiplayer-next.md index 45340349..bd22a464 100644 --- a/multiplayer-next.md +++ b/multiplayer-next.md @@ -1595,6 +1595,8 @@ The WebSocket contract and Go event hub now enforce opaque match and server IDs Assignment projection now rejects fractional `slot` and `protocol_version` values instead of truncating them, matching the OpenAPI integer contract. +Allocated `ServerConfig` startup now enforces the opaque match/server ID contract, rejecting short or unsafe allocation flags before process launch. + Signed MatchNet claims now also require exact JSON string/integer types for every identity, protocol, expiry, slot, team, and generation field; string-number coercion is rejected before canonical signature verification. Presentation progress: a shared `Game/themes/cosmic_clash_theme.tres` now gives the menu, lobby, matchmaking, and settings surfaces consistent button, input, option, and label styling. The custom-font portion of `TODO.md` remains open until a distributable font asset is selected.