Don't Trust The Client Part 1
Don't trust what you receive

Pop quiz! You're implementing a shop in a server authoritative multiplayer game. There's multiple items the player can buy, each with different prices. The player selects an item to purchase:

Item Selection:
Name: Brown Boots
Item ID: 42
Price: 450 gold
Player Data:
Name: Bob
Player ID: 3
Current Gold: 500

As the player selects their item, a network message goes out from the client to the server. What data do you include in that message?

The most straightforward thing you could do is:

Item ID: 42
Price: 450
Player ID: 3

Simple enough! Receive the data on the server, add ItemID to PlayerID's inventory, lower their gold by Price. Easy enough to implement.

First Exploit: Price Tampering

Now, of course, we shouldn't include the Price in the network message. If we do, the client could simply send us a tampered request like:

Exploit #1 - Free Items:
Item ID: 42
Price: 0
Player ID: 3

Free brown boots! Perhaps they can even send in a negative Price value.

Exploit #1b - Getting Paid:
Item ID: 42
Price: -9000
Player ID: 3

That's an even better deal, you're paying them to take the brown boots off your hands. Now that's a business plan!

So, let's get rid of the price. On the server, we will just use our Item ID to retrieve the full data for the item, including its price. A little bit more work, but we've removed a giant exploit.

Improved Message:
Item ID: 42
Player ID: 3

Second Exploit: Player ID Manipulation

However, the sneaky client can still perform an exploit. Free items aren't a thing anymore, but player 3 can now send this message:

Exploit #2 - Forcing Other Players to Buy:
Item ID: 42
Player ID: 7

Any player can get any other player to purchase any item! Just tamper the Player ID, and you can get some unlucky schmuck on the enemy team to buy several useless brown boots, depleting all his gold in the process.

So, well, we scratch that too. But if our network message only has the itemID, how do we know which player to give the item to? Well, the answer is more work on our part!

When players connect to the game, we will need to associate each connection with a corresponding player. Then, when we receive our purchase message, we determine the PlayerID of the sender based on what connection the message came from. This is a non-trivial identity/authentication problem, but you'll most likely be using a library that solves it for you. Mirror for Unity is what I use.

Final Message Format:
Item ID: 42

Player identity determined server-side from connection context.

Third Exploit: Invalid Item IDs

Well, that's even more complicated than what we started from, but at least now we should be good. We have a minimal network message. Surely this is foolproof. Well... Almost, but not quite there. Our network message is indeed as minimal as it can get, there's nothing else we can remove. But, while we do need some itemID from the client, we can't really trust that it is a correct itemID. The client tries these messages:

Exploit #3 - Invalid IDs:
Item ID: 9999999999

Uh oh, our item ID's only go up to 512. We take the itemID, try to retrieve our corresponding item. It doesn't exist. Our server crashes.

We add a check to make sure the provided ID is not out of bounds.

Fourth Issue: Missing Gameplay Validation

Client sends:

Item ID: 42

Finally a valid item! But, turns out the client has 0 gold. We have a check on the client graying out the button for items that the player can't afford, but Bob is obviously not stopped by that. We never added a check on the server to check if the player actually has enough gold, so Bob gets the item and has his gold value set to -450. Being broke doesn't matter if you can go into infinite debt.

We should also probably check if the player has free item slots, if the player is in range of a shop, every single gameplay condition. Once on the client to set UI state. Once on the server to verify against cheaters.

Fifth Issue: Rate Limiting

So we patch all that as well. Minimal data. Valid ID. Gameplay conditions met. Phew.

Client sends:

Item ID: 42

And then does it again. And again. One hundred thousand times per second. Our server crashes. We implement rate limiting to disconnect spamming players.

General Rules

Now, we have to repeat the same process for every single message the client sends to the server. There's no silver bullet, and you'll need to consider each case individually, but there are some rules of thumb that we've applied here:

Rule #1: Minimal Data

Take exactly as much data as you need from the client and no more. In practice, this should always consist of data communicating 'intention'.

  • I want to buy this item
  • I want to walk to this position
  • I want to use this ability
Rule #2: Validate Everything

Check the validity of all the data you do receive. Do you have enough gold to buy this item? Is the position you want to walk to actually somewhere you can navigate to? Is the ability you want to use off cooldown, and do you have enough mana?

In practice, this almost always means you have to do the validity check twice: Once on the client to set UI state, and once on the server to prevent tampering from cheaters.

Testing for Exploits

A useful trick for the second point is to design your UI and logic with a debug always-on setting in mind. When enabled, you should have all of your buttons and inputs be allowed, at all times. This lets you easily simulate what a cheating client would do, and is helpful in finding exploits you've missed.

In general, you should aim to design your own cheats where possible. It doesn't take too much work to hard-code a few tampered requests you can send whenever you want.


So that's that for data we receive from the client. Unfortunately, that's just half the work. We also have to worry about what we are sending to the client.

I cover that in part 2.


Links: