fix(multiplayer): validate ranked season expiry

This commit is contained in:
Josh Creek
2026-09-01 22:33:00 +01:00
parent d819658ace
commit ec0bc362cd
3 changed files with 18 additions and 2 deletions
+14 -2
View File
@@ -39,13 +39,25 @@ func apply(payload: Dictionary) -> bool:
provisional = bool(payload["provisional"])
season_id = String(payload.get("season_id", ""))
season_ends_at_unix = 0
if payload.has("season_ends_at") and payload["season_ends_at"] is String and not String(payload["season_ends_at"]).is_empty():
season_ends_at_unix = maxi(0, int(Time.get_unix_time_from_datetime_string(String(payload["season_ends_at"]))))
if payload.has("season_ends_at"):
if not payload["season_ends_at"] is String or not is_valid_season_timestamp(String(payload["season_ends_at"])):
return _reject("Profile response contains invalid season expiry")
var parsed_season_end := Time.get_unix_time_from_datetime_string(String(payload["season_ends_at"]))
if parsed_season_end < 0:
return _reject("Profile response contains invalid season expiry")
season_ends_at_unix = int(parsed_season_end)
available = true
error_message = ""
return true
static func is_valid_season_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
func set_error(reason: String) -> void:
available = false
error_message = reason
@@ -163,3 +163,5 @@ func test_ranked_profile_projects_and_bounds_season_countdown() -> void:
assert_true(profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 10, "tier": "GOLD", "provisional": false, "season_id": "s1", "season_ends_at": "1970-01-03T00:00:00Z"}), "season end applies")
assert_true(profile.display_text(1000).contains("Season ends in 2d"), "countdown rounds up remaining season time")
assert_true(profile.display_text(300000).contains("Season ends in 0d"), "expired season countdown is clamped")
assert_true(not profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 10, "tier": "GOLD", "provisional": false, "season_ends_at": "not-a-timestamp"}), "malformed season expiry is rejected")
assert_true(not profile.apply({"rating": 1500.0, "rd": 200.0, "volatility": 0.06, "ranked_games": 10, "tier": "GOLD", "provisional": false, "season_ends_at": 123}), "non-string season expiry is rejected")
+2
View File
@@ -1577,6 +1577,8 @@ Assignment expiry validation now fails closed on malformed non-empty timestamps
MatchNet join-authorisation admission now applies the same expiry format guard before parsing signed roster claims, closing the malformed-expiry gap at the transport handshake boundary.
Ranked profile season metadata now validates optional expiry type and RFC3339 format before deriving the UI countdown, rejecting malformed server projections instead of silently displaying a profile without season context.
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.