[Security][BE] Cloak/radar is client-side only — entity deltas + join roster leak stealthed ships to all clients #982

Closed
opened 2026-08-06 19:54:10 +00:00 by spikerj · 1 comment
Owner

Part of the Game Unification lane (EPIC #926/#927). Raised by Joey after the survey slice shipped: cloak/radar must be uncheatable — trust the server only.

Audit findings (spikersoft-backend master, 2026-08-06)

The cloak is currently client-side theater. Server-side stealth/radar logic (RadarVisibilityService.CanDetect, RadarContactTracker) is only applied to the radar-contact HUD events. The authoritative entity streams ignore it:

  1. SpaceZone.BroadcastEntityDeltas sends ALL ships + spacecraft to EVERY connection (ComputeBatchDeltaEvent(connectionId, spacecraft, ships, ...) — no filter). A cloaked ship's position/rotation/velocity/health streams to every client at up to 30 Hz; clients merely fade it visually. The Godot client even UPSERTS ships from unknown-id deltas, so a modified client renders every cloaked ship with zero effort.
  2. Join roster leak: BaseZone.AddPlayerAsync broadcasts PlayerJoined (name + exact position) to everyone and sends the full existing-player roster to the joiner — stealth state ignored.
  3. Target command accepts any entity id regardless of whether the observer can detect it (lock-through-cloak).

Not leaking (verified): StealthStateChanged is owner-only; radar contacts are server-computed; scan/survey validation is server-side.

Fix (server-authoritative visibility)

  • Per-connection visible-entity set in SpaceZone = {own ship + own spacecraft} ∪ {live radar contacts} (reuses RadarContactTracker — radar IS the sensor model; radar off = dark, matching the existing contact-blanking rule).
  • BroadcastEntityDeltas filters per connection; per-entity delta state cleared on visibility transitions so reappearing entities get a full snapshot.
  • Visibility transitions emit per-observer PlayerJoined/PlayerLeft (ships) and spacecraft announce/remove equivalents — clients already handle these; ghosts stay on the radar-contact channel (last-known only, no live deltas).
  • Space join path stops broadcasting/rostering unfiltered; announcements flow from the first visibility tick (~100 ms).
  • Target (Entity kind) rejected unless the target is in the observer's visible set.

Remaining lower-bandwidth channels to review in a follow-up: DamageDealt/HealthManaUpdate/beam + projectile events referencing entity ids of undetected ships (positions not included; acceptable short-term, listed for completeness).

Part of the **Game Unification** lane (EPIC #926/#927). Raised by Joey after the survey slice shipped: cloak/radar must be uncheatable — **trust the server only**. ## Audit findings (spikersoft-backend master, 2026-08-06) **The cloak is currently client-side theater.** Server-side stealth/radar logic (`RadarVisibilityService.CanDetect`, `RadarContactTracker`) is only applied to the radar-contact HUD events. The authoritative entity streams ignore it: 1. **`SpaceZone.BroadcastEntityDeltas` sends ALL ships + spacecraft to EVERY connection** (`ComputeBatchDeltaEvent(connectionId, spacecraft, ships, ...)` — no filter). A cloaked ship's position/rotation/velocity/health streams to every client at up to 30 Hz; clients merely fade it visually. The Godot client even UPSERTS ships from unknown-id deltas, so a modified client renders every cloaked ship with zero effort. 2. **Join roster leak**: `BaseZone.AddPlayerAsync` broadcasts `PlayerJoined` (name + exact position) to everyone and sends the full existing-player roster to the joiner — stealth state ignored. 3. `Target` command accepts any entity id regardless of whether the observer can detect it (lock-through-cloak). Not leaking (verified): `StealthStateChanged` is owner-only; radar contacts are server-computed; scan/survey validation is server-side. ## Fix (server-authoritative visibility) - Per-connection visible-entity set in SpaceZone = {own ship + own spacecraft} ∪ {live radar contacts} (reuses `RadarContactTracker` — radar IS the sensor model; radar off = dark, matching the existing contact-blanking rule). - `BroadcastEntityDeltas` filters per connection; per-entity delta state cleared on visibility transitions so reappearing entities get a full snapshot. - Visibility transitions emit per-observer `PlayerJoined`/`PlayerLeft` (ships) and spacecraft announce/remove equivalents — clients already handle these; ghosts stay on the radar-contact channel (last-known only, no live deltas). - Space join path stops broadcasting/rostering unfiltered; announcements flow from the first visibility tick (~100 ms). - `Target` (Entity kind) rejected unless the target is in the observer's visible set. Remaining lower-bandwidth channels to review in a follow-up: `DamageDealt`/`HealthManaUpdate`/beam + projectile events referencing entity ids of undetected ships (positions not included; acceptable short-term, listed for completeness).
spikerj added the agenticbug labels 2026-08-06 19:54:10 +00:00
Author
Owner

Verified complete 2026-08-07 — closing.

  • Code: spikersoft-backend@98102023 — the fix is on master via PR #525 (5c04ed99, merge c994553b,
    "fix(gameserver): server-authoritative visibility — cloaked ships never leave the server (#982)").
    All four bullets of the fix section are implemented:
    • per-connection visible set + filtered broadcast — SpikerSoft.GameServer/Zones/SpaceZone.cs:759-780
      (IsEntityVisibleTo at :1005, scratch lists filtered before ComputeBatchDeltaEvent at :781)
    • per-entity delta state cleared on visibility transitions — SpaceZone.cs:956, :989 →
      NetworkDeltaCompression.ClearClientEntityState (:238)
    • no join-roster broadcast in space — SpaceZone.cs:34 overrides BaseZone.RevealPlayersOnJoin to
      false; honoured at BaseZone.cs:932 (camp/dungeon unchanged)
    • Target (Entity kind) rejected unless visible — SpaceZone.cs:5164
    • 8 new SpikerSoft.GameServer.Tests/Zones/SpaceZoneVisibilityTests.cs drive the real pipeline
      (radar tick → transitions → filtered broadcast)
  • Live: docker service ps spikersoft-gameserver_spikersoft-gameserver → 1/1 Running on SERVER,
    image redeployed 2026-08-07 12:25 UTC, i.e. after the merge (2026-08-06 20:07 UTC). The deployed
    build therefore carries the fix.
  • Shipped by: spikerj/spikersoft-backend PR #525 / 5c04ed99 / merge c994553b.

Not migrated: the ticket's own fix scope is done and live.

The one thing this ticket deferred — "remaining lower-bandwidth channels to review in a follow-up:
DamageDealt/HealthManaUpdate/beam + projectile events referencing entity ids of undetected ships"
—
has been filed separately as spikerj/spikersoft-backend#547 so it isn't lost when this closes.

— Opus 5 Agent

Verified complete 2026-08-07 — closing. - **Code:** `spikersoft-backend@98102023` — the fix is on master via PR #525 (`5c04ed99`, merge `c994553b`, *"fix(gameserver): server-authoritative visibility — cloaked ships never leave the server (#982)"*). All four bullets of the fix section are implemented: - per-connection visible set + filtered broadcast — `SpikerSoft.GameServer/Zones/SpaceZone.cs:759-780` (`IsEntityVisibleTo` at `:1005`, scratch lists filtered before `ComputeBatchDeltaEvent` at `:781`) - per-entity delta state cleared on visibility transitions — `SpaceZone.cs:956`, `:989` → `NetworkDeltaCompression.ClearClientEntityState` (`:238`) - no join-roster broadcast in space — `SpaceZone.cs:34` overrides `BaseZone.RevealPlayersOnJoin` to `false`; honoured at `BaseZone.cs:932` (camp/dungeon unchanged) - `Target` (Entity kind) rejected unless visible — `SpaceZone.cs:5164` - 8 new `SpikerSoft.GameServer.Tests/Zones/SpaceZoneVisibilityTests.cs` drive the real pipeline (radar tick → transitions → filtered broadcast) - **Live:** `docker service ps spikersoft-gameserver_spikersoft-gameserver` → 1/1 Running on SERVER, image redeployed **2026-08-07 12:25 UTC**, i.e. after the merge (2026-08-06 20:07 UTC). The deployed build therefore carries the fix. - **Shipped by:** spikerj/spikersoft-backend PR #525 / `5c04ed99` / merge `c994553b`. Not migrated: the ticket's own fix scope is done and live. The one thing this ticket deferred — *"remaining lower-bandwidth channels to review in a follow-up: `DamageDealt`/`HealthManaUpdate`/beam + projectile events referencing entity ids of undetected ships"* — has been filed separately as **spikerj/spikersoft-backend#547** so it isn't lost when this closes. — Opus 5 Agent
Sign in to join this conversation.