fix(multiplayer): reject fractional assignment values

This commit is contained in:
Josh Creek
2026-09-01 22:42:46 +01:00
parent e376e1e80d
commit 75f9026ea1
3 changed files with 17 additions and 1 deletions
+9 -1
View File
@@ -21,7 +21,7 @@ func apply(payload: Dictionary, expected_player_id: String = "") -> bool:
for key in ["match_id", "server_id", "player_id", "slot", "expires_at", "protocol_version", "transport", "endpoint", "join_authorisation"]:
if not payload.has(key):
return _reject("Assignment response is missing " + key)
if not payload["match_id"] is String or not payload["server_id"] is String or not payload["player_id"] is String or not (payload["slot"] is int or payload["slot"] is float) or not payload["expires_at"] is String or not (payload["protocol_version"] is int or payload["protocol_version"] is float) or not payload["transport"] is String or not payload["endpoint"] is String or not payload["join_authorisation"] is String:
if not payload["match_id"] is String or not payload["server_id"] is String or not payload["player_id"] is String or not _valid_nonnegative_integer(payload["slot"]) or not payload["expires_at"] is String or not _valid_nonnegative_integer(payload["protocol_version"]) or not payload["transport"] is String or not payload["endpoint"] is String or not payload["join_authorisation"] is String:
return _reject("Assignment response contains invalid types")
var next_match_id := String(payload["match_id"])
var next_server_id := String(payload["server_id"])
@@ -71,6 +71,14 @@ static func _valid_endpoint(value: String) -> bool:
return port.is_valid_int() and int(port) >= 1 and int(port) <= 65535
static func _valid_nonnegative_integer(value: Variant) -> bool:
if value is int:
return int(value) >= 0
if value is float:
return is_finite(float(value)) and float(value) >= 0.0 and float(value) == floor(float(value))
return false
func _reject(reason: String) -> bool:
available = false
error_message = reason
@@ -21,6 +21,12 @@ func test_assignment_projection_rejects_wrong_shape_or_unsafe_transport() -> voi
var short_id := valid.duplicate()
short_id["match_id"] = "match-1"
assert_true(not assignment.apply(short_id), "short opaque assignment id is rejected")
var fractional_slot := valid.duplicate()
fractional_slot["slot"] = 1.5
assert_true(not assignment.apply(fractional_slot), "fractional slot is rejected")
var fractional_protocol := valid.duplicate()
fractional_protocol["protocol_version"] = 1.5
assert_true(not assignment.apply(fractional_protocol), "fractional protocol version is rejected")
assert_true(not assignment.available, "invalid assignment is not exposed")
assert_true(not assignment.apply({"match_id": "match-1", "server_id": "server-1", "player_id": "player-1", "slot": 0, "expires_at": "future", "protocol_version": 1, "transport": "udp", "join_authorisation": "signed"}), "unknown transport is rejected")
assert_true(not assignment.apply({"match_id": "match-1", "server_id": "server-1", "player_id": "player-1", "slot": 0, "expires_at": "future", "protocol_version": 1, "transport": "steam_sdr", "join_authorisation": ""}), "empty authorisation is rejected")
+2
View File
@@ -1593,6 +1593,8 @@ Assignment projections and assignment-changed events now enforce the published o
The WebSocket contract and Go event hub now enforce opaque match and server IDs on assignment notifications, keeping server publication aligned with the Godot client validator.
Assignment projection now rejects fractional `slot` and `protocol_version` values instead of truncating them, matching the OpenAPI integer contract.
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.