Don't Trust The Client Part 2
Don't trust what you send

This is the followup to my post about not trusting the data you receive from clients.

What you can send over to the client depends a lot on your game design. For some games, all the gameplay information is supposed to be visible at all times for all clients. Take chess as an example: There's no hidden information, everything is known to both players.

However, in many games, information about the opponent is meant to be opaque. For example, a card game: You're not supposed to know what cards your opponent has in hand.

This is easy enough to engineer though. You just make sure each player only receives data about what is in their own hand. The only pitfall is accidentally sending everything to everyone, and then just hiding the opponents cards on the client-side logic.

The Fog of War Problem

There's more complicated examples though. Whenever there's data that can be known by the opponent, but only sometimes. For example: Any game with fog-of-war, like an RTS, or a MOBA. If an enemy unit isn't within your vision range, you don't know where they are and what they are doing.

Fog of war example in an RTS game

If you're simply sending all unit positions and state to both clients, and doing something on the client-side to hide units in the fog-of-war, then that's a pretty easy exploit. It won't take much for someone to implement a full-vision map-hack.

One solution is to only send data about a unit to player A, when the server knows that unit is within player A's vision. This sort of thing works fine for turn-based games, but it comes with a very large downside in real-time games. Server-to-client latency!

Client Prediction vs Security

Most real-time multiplayer games do some degree of client prediction — Meaning, they don't wait for the server to tell them about every single thing on the screen before they update state. If they did, everything would have more of a delay, and would feel sluggish. Instead of being instant, starting a movement would need to wait until your request reaches the server, the server processes it and send a message back, that message reaches your client, and finally your client processes it and renders a frame.

That time depends a lot on your ping, but for most games and most people it'll be at least a tenth of a second. Enough to feel sluggish and unresponsive.

Mechanics like fog-of-war have to deal with the same issue. Our two scenarios are:

Scenario 1: Send All Data (Exploitable)

Approach: We send all data about all unit positions to all clients. This is exploitable for map-hacks.

Upside: If I pass a corner, ending up in a position where I can see an enemy unit, I see that unit instantly. My client already knew that unit was there, I processed my movement on the client, and so I see the enemy unit right away.

Scenario 2: Withhold Data (Secure but Laggy)

Approach: We withhold data until the server knows we have vision of the unit. No map-hack possible, but...

Downside: If I pass a corner, ending up in a position I can see an enemy unit, there's a delay until the server processes my new position, notices that I can now see the enemy unit, sends that data back to me, and my client updates my vision with the new data. As a client I will experience the enemy unit popping into existence about a tenth of a second after I turn the corner, instead of being there right away.

The Compromise Solution

This is a pretty tough nut to crack. There's really no perfect solution. A reasonable solution trying to balance the two would be to send data for all units that are in vision, and all units that are almost in vision. It still kind of allows for a map-hack exploit, but a significantly weaker one.

The trade-off depends a lot on exactly how you define "almost in vision". A too-permissive definition still allows a map-hack exploiter to see enemy units a few seconds before an ambush. An insufficiently permissive definition defeats the purpose of the system, and maintains the popping into vision problem. The perfect balance depends a lot on the specific game its being built for. Most likely, there is not perfect solution.

The Reality of Multiplayer Games

If you look closely enough, all multiplayer games have a bunch of weird little edge cases in them. Artifacts of the engineer's struggle to balance responsiveness, latency, and security.

Most people won't notice them, at least not consciously, but they probably will notice them unconsciously. Despite that, we're all working with limited time and resources. We can't make everything perfect, even if it would be fun to do so.

Key Takeaway:

You don't have to be perfect about any of this. But you should always be thinking about it. Making compromises is fine, but they should be made deliberately, after understanding the problem and weighing the possibilities.

The Cost of Ignoring Security:

If you're always thinking about what could be exploited, you'll end up with a few potential exploits, and a few tradeoffs.

If you're just worrying about getting the game working now, and hoping you'll patch security later, you'll end up with more security holes than you can count. Some of those security holes will end up being load-bearing for the rest of your code. You'll find yourself either stuck with broken systems, or forced to do significant rewrites.


So, try to keep security in mind as you're building your game, from day 1. It'll save you a lot of trouble in the long run.


Related Reading: