Close #8860: Server checks who may own a unit, on every path that changes it - #8962
Close #8860: Server checks who may own a unit, on every path that changes it#8962HammerGS wants to merge 4 commits into
Conversation
Closes #8860 receiveEntityAdd took the owner from the payload and never compared it to the sending connection. The client offers only legal recipients, but a rule enforced on one side only is a rule the other side cannot rely on: two unrelated client changes once combined to hand every connecting player's units to the host for several days. Rule, as agreed: the owner is the sender, or the sender holds Gamemaster, or the owner is a bot. A refused unit is dropped and named in chat, which is how an illegal design is already handled a few lines above. The bot clause is the known weak point. Since #8808 a unit for your own Princess travels over your connection carrying the bot's owner id, and the server has no record of which human runs which bot, so it cannot tell your bot from anyone else's. Every hand-off to a bot is logged rather than passed over silently. Closing it properly needs clients to register the bots they run, which is its own change. Verified against MekHQ before writing the guard: AtBGameThread sets entity.setOwner to the sending client's own player on all three paths, and sends bot forces over the bot's own connection, so MekHQ satisfies the strictest clause and needs no exemption. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new server-side ownership rule needs automated test coverage on the existing ENTITY_ADD packet path to prevent regressions, and there is also a documentation/Javadoc placement issue in the changed region.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR hardens the Total Warfare server’s ENTITY_ADD handling by validating that the sending connection is allowed to assign the incoming entities’ owners, instead of trusting the owner id provided by the client payload (closing #8860).
Changes:
- Added server-side ownership authorization logic (
mayAddUnitFor) for incomingENTITY_ADDentities. - Enforced the authorization rule in
receiveEntityAddby dropping unauthorized entities and reporting via server chat/logs.
File summaries
| File | Description |
|---|---|
| megamek/src/megamek/server/totalWarfare/TWGameManager.java | Adds and enforces server-side validation of entity ownership on ENTITY_ADD packets, with logging/chat feedback when rejected. |
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (!mayAddUnitFor(entity, connIndex)) { | ||
| entities.remove(entity); | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Added in f48f825 as EntityAddOwnershipTest, following the C3MulLoadTest pattern you pointed at, so it drives the real ENTITY_ADD packet path rather than calling the guard directly.
Five cases: adding for yourself, for another human, as a gamemaster, for a bot, and for a player who is not in the game. Verified by disabling the guard, where the two refusal cases fail and the three permitted ones still pass.
| @@ -26351,6 +26351,59 @@ void updateVisibilityIndicator(Map<UnitTargetPair, LosEffects> losCache) { | |||
| * @param packet the packet to be processed | |||
| * @param connIndex the id for connection that received the packet. | |||
There was a problem hiding this comment.
Confirmed, the old block was left describing the wrong method. Moved back onto receiveEntityAdd in f48f825.
Adds EntityAddOwnershipTest, driving the real ENTITY_ADD packet path: adding for yourself, for another human, as a gamemaster, for a bot, and for a player who is not in the game. The two refusal cases fail with the guard disabled. Moves the receiveEntityAdd javadoc back onto receiveEntityAdd. Adding mayAddUnitFor above it left the old block describing the wrong method. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y1Lb2sbXg7jYcZoGDgyhWB
The guard covered adding units and nothing else. Three packets change ownership and all three took the new owner from the payload: ENTITY_ADD receiveEntityAdd was guarded ENTITY_ASSIGN receiveEntitiesAssign no check at all FORCE_ASSIGN_FULL receiveForceAssignFull no check at all Both reassignment handlers took the sending connection id and never looked at it. Transferring is the ordinary way to give a unit to your bot, so the lock was on the door nobody uses. The rule now lives in UnitOwnershipRules and all three call it. A reassignment asks it twice: may the sender act for the current owner, and may the recipient receive. A force moves whole, so one unit the sender may not give refuses the force rather than moving part of it. Every decision is logged, not only refusals, with the clause that applied. A permitted hand-off that leaves no trace cannot be audited afterwards or verified in testing, which is how the missing coverage went unnoticed. Four tests added for the reassignment path. The two refusal cases fail with the new guard disabled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y1Lb2sbXg7jYcZoGDgyhWB
|
Tested in game and working. Out of draft. Scope grew during testing, and it needed to. The guard originally covered adding units only. Transferring a unit is a different packet, All three now share one rule in Verified with a host, a remote client and a Princess bot. Nine tests, refusal cases confirmed by disabling the guard. |
What this changes
The server now decides who may own the units a client sends, instead of trusting the payload.
Three packets change ownership, and all three read the new owner straight out of the packet:
ENTITY_ADDreceiveEntityAddENTITY_ASSIGNreceiveEntitiesAssignconnIdand never used itFORCE_ASSIGN_FULLreceiveForceAssignFullThe client offers only legal recipients, but a rule enforced on one side only is a rule the other side cannot rely on: two unrelated client changes once combined to hand every connecting player's units to the host for several days.
The rule, as agreed on the issue: the owner is the sender, or the sender holds Gamemaster, or the owner is a bot. It now lives in
UnitOwnershipRulesand all three handlers call it, so there is one rule rather than three chances to get it wrong.A reassignment asks it twice, because giving a unit away and receiving one are separate questions: may the sender act for the unit's current owner, and may the recipient receive it. A force moves whole, so one unit the sender may not give refuses the force rather than moving part of it. A refused hand-off is named in chat, which is how an illegal design is already handled.
Every decision is logged, not only refusals, with the clause that applied:
A permitted hand-off that leaves no trace cannot be audited afterwards, and cannot be tested either. That is not incidental: the missing coverage on the reassignment paths went unnoticed precisely because exercising them produced silence.
Closes #8860
The bot clause
That clause is the known weak point and it is deliberate. Since #8808 a unit for your own Princess travels over your connection carrying the bot's owner id, and the server holds no record of which human runs which bot, so it cannot tell your bot from anyone else's. Every hand-off to a bot is logged rather than passed over in silence.
Closing it properly means clients registering the bots they run, which touches connection lifecycle and reconnects. That is its own change.
Testing
Tested in game with a host, a remote second client and a Princess bot. All three permitted clauses were exercised and the log agrees with the client's own recipient list:
No refusals during ordinary play, and nothing new in the log beyond the usual startup noise.
compileJava,checkstyleMain,checkstyleTest,spotlessApplyandjavadocpass.EntityAddOwnershipTestis 9 tests, 0 failures, driving the real packet path throughhandlePacket: adding for yourself, for another human, as a gamemaster, for a bot, for a player not in the game, and the four reassignment cases. The refusal cases fail with the guard disabled, checked for both the add and the reassign paths.What is not proven yet
AtBGameThreadsets the owner to the sending client's own player on all three paths and sends bot forces over the bot's own connection, so MekHQ satisfies the strictest clause, but that is from reading its code rather than running it.