Skip to content

Hidden-zone card names leak to opponents: hide_card never clears GameObject::base_name #7201

Description

@lgray

Summary

GameObject::base_name is never cleared by the hidden-information filter, so every card in a hidden zone ships its real printed name to every opponent over the wire. A client that reads base_name instead of name sees the full contents of opponents' hands and libraries.

This is a hidden-information violation, not a display bug. Per CR 400.2: "Hidden zones are zones in which not all players can be expected to see the cards' faces. Library and hand are hidden zones, even if all the cards in one such zone happen to be revealed."

Evidence

Verified at a700239ef, in crates/engine/src/game/:

  1. Every object's base_name is its real name. game_object.rs:2190GameObject::new constructs with base_name: name.clone(). There is also a backfill at :2077-2078 (if self.base_name.is_empty() && !self.name.is_empty() { self.base_name = self.name.clone(); }), so an empty base_name refills itself.

  2. hide_card redacts name but not base_name. visibility.rs::hide_card sets obj.name = HIDDEN_CARD_NAME and clears abilities, keywords, base_keywords, power, toughness, loyalty, color, base_color, trigger_definitions, replacement_definitions, static_definitions, casting_permissions, printed_ref, base_printed_ref, back_face, token_image_ref, source_related_token_ids, foretold. base_name is absent from that list.

  3. base_name is serialized. Declared #[serde(default)] pub base_name: String with no skip_serializing_if, so it is present in the filtered payload sent to each viewer.

  4. base_name is load-bearing, not vestigial — game_object.rs:2465 restores self.name = self.base_name.clone(), which is why it holds the true name (it is the revert target for temporary name changes such as copy effects).

Impact

An opponent's client receives, for each card in your hand and library, an object whose name reads "Hidden Card" while base_name carries the real card name. That is complete knowledge of hidden zones — hand contents, library contents, and by extension your remaining outs. It applies to every hide_card call site.

Root cause — the reason this will recur

hide_card is an allowlist-shaped problem implemented as a denylist: it enumerates fields to clear rather than projecting a minimal object. Any field added to GameObject therefore defaults to leaked, and nothing in the type system or test suite objects.

Note specifically that _gameobject_partition_is_total does not protect against this. That guard forces every new field into a serialization classification; it says nothing about visibility. A field can pass the partition guard and still leak.

Other fields with the same defect

Measured on the wire (sentinels set on a hidden card, filter_state_for_viewerserde_json::to_string) during the work described below. base_name I re-verified directly from source as above; the rest were observed leaking and are all likewise absent from hide_card's clear list:

field note
base_name real card name — the sharpest one
spellbook list of card names
unimplemented_mechanics per-card mechanic fingerprint
token_rules_text
base_power / base_toughness sentinel values survived redaction

Additionally base_card_types, base_mana_cost, base_abilities, base_*_definitions, base_loyalty, base_defense, card_id, mana_cost, card_types are serialized and uncleared; they were empty in the synthetic probe, but apply_card_face_to_object populates them from the real card face in a live game, so they should be treated as suspect until measured.

Frontend blast radius (for triage): base_name and spellbook have zero frontend consumers; unimplemented_mechanics has three. So clearing the two sharpest fields is unlikely to affect rendering.

Steps to reproduce

  1. Build a state where P1 has cards in hand.
  2. let filtered = filter_state_for_viewer(&state, P2);
  3. Inspect any P1 hand object: name == "Hidden Card", but base_name is the real card name.
  4. serde_json::to_string(&filtered) — the real name is in the payload.

Suggested fix

Clearing base_name alone is a one-line stopgap and is worth doing immediately, but it leaves the class open. The structural fix is to project a minimal object for hidden cards — construct the redacted view from an explicit whitelist of fields a non-viewer is allowed to see — so that a newly added field defaults to hidden rather than leaked. That change wants its own test matrix plus a client-consumer audit, which is why this is filed separately rather than bundled.

Whichever route, the regression should be a matched pair: the field is empty for a non-viewer and populated for the owner. Asserting only the empty case passes vacuously if the field was never populated in the fixture.

Worth auditing the sibling redactor redact_face_down_identity_from_observer in the same pass — it had the identical omission for a different field (see below).

Provenance

Found while addressing review feedback on #7101. That PR added a parse_warnings field to GameObject and it leaked by exactly this mechanism — an opponent received name: "Hidden Card" alongside diagnostics containing the card's verbatim rules text. #7101 fixes its own regression in both hide_card and redact_face_down_identity_from_observer; the pre-existing fields listed above are deliberately not fixed there, because the correct repair is the structural one described above.

Related

Related to #6732 (Face down cards largely unplayable). Both are defects in the same face-down / hidden-information redaction layer, in opposite directions: #6732 reports face-down cards being unreadable to the player who should be able to identify them, while this issue is hidden cards being readable by players who should not. Taken together they suggest the redaction seam needs an explicit per-viewer visibility contract rather than incremental field-by-field adjustment.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions