fix(multiplayer): validate event timestamps

This commit is contained in:
Josh Creek
2026-09-01 22:21:18 +01:00
parent 73e5d64ba2
commit 87d43302e1
3 changed files with 17 additions and 3 deletions
+9 -3
View File
@@ -282,8 +282,7 @@ static func is_valid_access_token(token: String) -> bool:
static func is_session_expired(expires_at: String, now_unix: int = -1) -> bool:
if expires_at.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})$")
if timestamp_pattern.search(expires_at) == null:
if not is_valid_rfc3339_timestamp(expires_at):
return true
var expiry_unix := Time.get_unix_time_from_datetime_string(expires_at)
if expiry_unix < 0:
@@ -294,6 +293,13 @@ static func is_session_expired(expires_at: String, now_unix: int = -1) -> bool:
return expiry_unix <= current_unix
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
static func is_retryable_mutation_response(response_code: int) -> bool:
return response_code == 0 or response_code == HTTPClient.RESPONSE_REQUEST_TIMEOUT or response_code == HTTPClient.RESPONSE_TOO_MANY_REQUESTS or response_code >= 500
@@ -470,7 +476,7 @@ static func _valid_websocket_event(event: Dictionary) -> bool:
return false
if not event.has("resource_id") or not event["resource_id"] is String or String(event["resource_id"]).is_empty():
return false
if not event.has("occurred_at") or not event["occurred_at"] is String or String(event["occurred_at"]).is_empty():
if not event.has("occurred_at") or not event["occurred_at"] is String or not is_valid_rfc3339_timestamp(String(event["occurred_at"])):
return false
var event_name := String(event["event"])
if event_name == "assignment_changed":
@@ -48,6 +48,9 @@ func test_session_expiry_is_checked_at_the_boundary_and_fails_closed() -> void:
assert_true(not ControlPlaneClient.is_session_expired("1970-01-01T00:16:40Z", 999), "session remains valid before expiry")
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-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")
func test_websocket_event_validation_requires_contract_specific_fields() -> void:
@@ -73,6 +76,9 @@ func test_websocket_event_validation_requires_contract_specific_fields() -> void
var negative := envelope.duplicate()
negative["revision"] = -1
assert_true(not ControlPlaneClient._valid_websocket_event(negative), "negative event revision is rejected")
var malformed_time := envelope.duplicate()
malformed_time["occurred_at"] = "yesterday"
assert_true(not ControlPlaneClient._valid_websocket_event(malformed_time), "malformed event timestamp is rejected")
func test_websocket_reconnect_defers_recovery_while_http_mutation_is_in_flight() -> void:
+2
View File
@@ -1560,3 +1560,5 @@ Ticket projections now validate playlist metadata on every update, rejecting unk
Client sessions now fail closed at the expiry boundary and proactively clear credentials before reconnects or authenticated requests. Boundary and malformed-expiry tests cover the lifecycle guard.
Queue heartbeat and cancellation revision conflicts now schedule the same authoritative ticket recovery as proposal conflicts, preventing stale client actions from leaving the visible queue state unresolved. Adversarial operation/status/identity coverage is included.
WebSocket event envelopes now require RFC3339 timestamps rather than merely non-empty text, matching the versioned contract; session-expiry format checks use the same boundary validator. Malformed-format adversarial coverage is included.