cg-bid-net: refuse a discard of cards the player does not hold

Finding 3 of the 2026-07-29 security review: bids, passes and plays
were validated properly, but a kitty discard only checked that five
cards were named -- and cg-bid--discard removes cards with
cl-set-difference, which silently ignores cards not in the hand.  A
client naming five cards it did not hold kept all fifteen and played
on with more cards than the rules allow.

New cg-bid--net-holds-p checks every named card against the seat's
hand, respecting multiplicity (five copies of one held card do not
pass).  The check joins the phase/contractor/arity checks that already
live in the discard branch of cg-net-apply-move.

Test cgt-bid-net-discard-checked fails against the previous code: the
foreign discard was accepted (returned t) and the game advanced to
play with a 15-card hand.
This commit is contained in:
Claude 2026-08-03 20:32:45 -05:00 committed by Corwin Brust
parent f8873fb8fa
commit 3b55d780b0
2 changed files with 40 additions and 1 deletions

View file

@ -143,6 +143,17 @@ and a hand exposed by an open misère is revealed to everyone."
;;;; Apply a move on the host
(defun cg-bid--net-holds-p (hand cards)
"Return non-nil when every card in CARDS is present in HAND.
Multiplicity counts: naming one held card five times is not holding
five cards. Cards are (SUIT . RANK) conses compared with `equal'."
(let ((left (copy-sequence hand)))
(catch 'missing
(dolist (c cards t)
(if (member c left)
(setq left (cl-remove c left :test #'equal :count 1))
(throw 'missing nil))))))
(cl-defmethod cg-net-apply-move ((game cg-bid-game) seat move)
"Apply MOVE made by absolute SEAT to the host's 500 GAME.
MOVE is (bid BID), (pass), (discard CARD...) or (play CARD). Return
@ -157,7 +168,8 @@ non-nil when the move was legal and applied, so the host broadcasts."
(cg-bid--auction-act game seat nil) (setq ok t)))
(`(discard . ,cards)
(when (and (eq phase 'kitty) (eql (cg-get game :contractor) seat)
(= (length cards) 5))
(= (length cards) 5)
(cg-bid--net-holds-p (cg-bid--hand game seat) cards))
(cg-bid--discard game seat cards) (setq ok t)))
(`(play ,card)
(when (and (eq phase 'play) (eql (cg-get game :turn) seat)