From bfaf5d40ff2cbef00be79e447bc410ca19059690 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 1 Sep 2026 23:02:31 +0100 Subject: [PATCH] fix(multiplayer): validate timestamp calendar --- Game/scripts/control_plane_client.gd | 26 ++++++++++++++++++- Game/tests/cases/test_control_plane_client.gd | 2 ++ multiplayer-next.md | 2 ++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Game/scripts/control_plane_client.gd b/Game/scripts/control_plane_client.gd index 2f3f2ac2..6a57443f 100644 --- a/Game/scripts/control_plane_client.gd +++ b/Game/scripts/control_plane_client.gd @@ -305,7 +305,31 @@ static func is_valid_rfc3339_timestamp(value: String) -> bool: if value.is_empty(): return false var timestamp_pattern := RegEx.create_from_string("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$") - return timestamp_pattern.search(value) != null + if timestamp_pattern.search(value) == null: + return false + var year := int(value.substr(0, 4)) + var month := int(value.substr(5, 2)) + var day := int(value.substr(8, 2)) + var hour := int(value.substr(11, 2)) + var minute := int(value.substr(14, 2)) + var second := int(value.substr(17, 2)) + if month < 1 or month > 12 or hour > 23 or minute > 59 or second > 59: + return false + var days_in_month := [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + var leap_year := year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) + if leap_year: + days_in_month[1] = 29 + if day < 1 or day > days_in_month[month - 1]: + return false + var timezone_index := value.find("+", 19) + if timezone_index < 0: + timezone_index = value.find("-", 19) + if timezone_index >= 0: + var offset_hour := int(value.substr(timezone_index + 1, 2)) + var offset_minute := int(value.substr(timezone_index + 4, 2)) + if offset_hour > 23 or offset_minute > 59: + return false + return true static func is_valid_resource_id(value: String) -> bool: diff --git a/Game/tests/cases/test_control_plane_client.gd b/Game/tests/cases/test_control_plane_client.gd index 57b6fa1e..5bff50c2 100644 --- a/Game/tests/cases/test_control_plane_client.gd +++ b/Game/tests/cases/test_control_plane_client.gd @@ -51,6 +51,8 @@ func test_session_expiry_is_checked_at_the_boundary_and_fails_closed() -> void: assert_true(ControlPlaneClient.is_session_expired("1970-01-01T00:16:40Z", 1000), "session expires at the exact boundary") assert_true(ControlPlaneClient.is_session_expired("not-a-timestamp", 1000), "malformed non-empty expiry fails closed") assert_true(ControlPlaneClient.is_valid_rfc3339_timestamp("2026-08-31T12:00:00.123Z"), "fractional RFC3339 timestamp is accepted") + assert_true(not ControlPlaneClient.is_valid_rfc3339_timestamp("2026-02-30T12:00:00Z"), "impossible calendar date is rejected") + assert_true(not ControlPlaneClient.is_valid_rfc3339_timestamp("2026-13-01T12:00:00Z"), "impossible month is rejected") assert_true(not ControlPlaneClient.is_valid_rfc3339_timestamp("2026-08-31 12:00:00Z"), "space-separated timestamp is rejected") assert_true(not ControlPlaneClient.is_valid_rfc3339_timestamp("2026-08-31T12:00:00"), "timezone-less timestamp is rejected") var valid_session := {"player_id": "player_1234567890", "access_token": "session-id:opaque-token", "expires_at": "2099-08-31T12:00:00Z"} diff --git a/multiplayer-next.md b/multiplayer-next.md index b4b53b2f..0a0210bc 100644 --- a/multiplayer-next.md +++ b/multiplayer-next.md @@ -1617,6 +1617,8 @@ The public `/api/v1` route adapters now reject non-opaque queue, proposal, assig The public queue adapter also rejects an explicitly supplied short or unsafe `ticket_id`; omitted IDs continue to be deterministically server-assigned for idempotent retries. +RFC3339 validation now checks both wire syntax and calendar parseability, rejecting impossible dates before they can become epoch metadata. + 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.