Adding Security and Fair Play - Multiplayer Game Development with HTML5 (2015)

Multiplayer Game Development with HTML5 (2015)

Chapter 6. Adding Security and Fair Play

Although we're only now talking about security, the main takeaway from this chapter is that security should be baked into your games. Just like other types of software, you can't just go in after the fact, slap in a few security features and expect the product to be bullet proof. However, since the primary focus of this book was not security, I think we can be justified in not bringing up the subject until the very last chapter.

In this chapter, we will discuss the following principles and concepts:

· Common security vulnerabilities in web-based applications

· Using Npm and Bower to add extra security to your games

· Making games more secure and less prone to cheating

Common security vulnerabilities

If you're coming to game development from one of the many other areas of software development, you will be pleased to know that securing a game is not much different than securing any other type of software. Treating a game like any other type of software that needs security, especially like a distributed and networked one, will help you to put in place the proper measures that will help you to protect your software.

In this section, we will cover a few of the most basic and fundamental security vulnerabilities in web-based applications (including games) as well as ways to protect against them. However, we will not delve too deeply (or at all) into more complicated networking security topics and scenarios, such as social engineering, denial of service attacks, securing user accounts, storing sensitive data properly, protecting virtual assets, and so on.

Encryption over the wire

The first vulnerability about which you should be aware of is that sending data from your server to your clients leaves the data exposed for others to see. Monitoring network traffic is almost as easy as walking and chewing gum at the same time, even though not everyone is skilled enough to do either of these things.

Here's a common scenario that you might require your players to go through while they play (or prepare to play) your game:

· The player enters a username and password in order to be authorized into your game

· Your server validates the login information

· The player is then allowed to continue on and play the game

If the initial HTTP request sent to the server by the player is not encrypted, then anyone who was looking at the network packets would know the user credentials and your player's account would be compromised.

The easiest solution is to transmit any such data over HTTPS. While using HTTPS won't solve all of one's security problems, it does provide us with a fairly certain guarantee, which includes the following points:

· The server responding to the client request would be who it says it is

· The data received by both the server and the client would not have been tampered with

· Anyone looking at the data wouldn't be able to read it in plain text

Since HTTPS packets are encrypted, anyone monitoring the network will need to decrypt each packet in order to know data that it contained, thus making it a safe way to send one's password to the server.

Just as there is no such thing as a free meal, there is also no such thing as free encryption and decryption. This means that by using HTTPS you will incur some measurable performance penalty. What this penalty actually is and how negligible it will be is highly dependent on a series of factors. The key is to evaluate your specific case and determine where exactly the use of HTTPS would be too expensive in terms of performance.

However, remember that at a very minimum, trading off performance for security is worth the cost when the value of the data is greater than the extra performance. It is possible that you may not be able to transmit thousands of players' positions and velocities per second over HTTPS because of the associated latency this would cause. However, each individual user will not be logging in very often after one initial authentication, so forcing at least that to be secure is certainly something nobody can afford to go without.

Script injection

The base principle behind this vulnerability is that your script takes user input as text (data) and evaluates it as code in an execution context. A typical use case for this is as follows:

· The game asks for the user to enter his/her name

· A malicious user enters code

· The game optionally saves that text for future use

· The game eventually uses that code in an execution context

In the case of web-based application, or more specifically with JavaScript being executed in a browser, the vicious input might be a string of HTML and the execution context the DOM. One particular feature of the DOM API is its ability to set a string as an element's HTML content. The browser takes that string and turns it into live HTML, just like any other HTML document that is rendered in some server.

The following code snippet is an example of an application that asks for a user's nickname, then displays it on the upper-right corner of the screen. This game may also save the player's name in a database and attempt to render that string with the player's name in other parts of the game as well:

/**

* @param {Object} player

*/

function setPlayerName(player){

var nameIn = document.getElementById('nameIn');

var nameOut = document.getElementById('nameOut');

player.name = nameIn.value;

// Warning: here be danger!

nameOut.innerHTML = player.name;

}

Script injection

For the casual developer, this seems like a rather lovely greeting to a player who is getting ready to enjoy your platform game. Provided that the user enters an actual name with no HTML characters in it, all will be well.

However, if the user decides to call himself something like <script src="http://my-website.com/my-script.js"></script> instead and we don't sanitize that string in order to remove characters that would make the string a valid HTML, the application could become compromised.

The two possible ways to exploit this vulnerability as a user are to alter the client's experience (for example, by entering an HTML string that makes the name blink or downloads and plays an arbitrary MP3 file), or to input an HTML string that downloads and executes JavaScript files that alter the main game script and interacts with the game's server in malicious ways.

To make matters worse, if we're not careful with protecting against other vulnerabilities, this security loophole can be exploited in conjunction with other vulnerabilities, further compounding the damage that can be done by an evil player:

Script injection

Server validation

Depending on how we process and use input from our users on the server, we can compromise the server and other assets by trusting the user with unsanitized input. However, just making sure that the input is generally valid is not enough.

For example, at some point you will tell the server where the player is, how fast and in which direction he or she is moving, and possibly which buttons are being pressed. In case we need to inform the server about the player's position, we would first verify that the client game has submitted a valid number:

// src/server/input-handler.js

socket.on(gameEvents.server_userPos, function(data){

var position = {

x: parseFloat(data.x),

y: parseFloat(data.y)

};

if (isNaN(position.x) || isNan(position.y) {

// Discard input

}

// ...

});

Now that we know that the user hasn't hacked the game to send in malicious code instead of their actual location vector, we can perform calculations on it and update the rest of the game state. Or, can we?

If the user sends invalid floats for their position, for example (assuming that we're working with floating point numbers in this case), we can simply discard the input or perform a specific action in response to their attempt to enter invalid values. However, what would we do if the user sends an incorrect location vector?

It could be that the player is moving along from the left side of the screen to the right. First, the server receives the player's coordinates showing where the player really is, then the player reports to being slightly further to the right and a little closer to a fiery pit. Suppose that the fastest the player can possibly move is, say, 5 pixels per frame. Then, how would we know whether the player truly did jump over and across the fiery pit in one frame (which is an impossible move) or whether the player cheated, if all we know is that the player sent a valid vector saying {x: 2484, y:4536}?

Server validation

The key principle here is to validate whether the input is valid. Note that we're talking about validating and not sanitizing user input although the latter is also indispensable and goes hand in hand with the former.

In its simplest form, one solution to the previous problem with a player reporting a fake position is that we could simply keep track of the last reported position and compare it to the one that is received next. For a more complex solution, we could keep track of the several previous positions and take a look at how the player is moving.

var PlayerPositionValidator = function(maxDx, maxDy) {

this.maxDx = maxDx;

this.maxDy = maxDy;

this.positions = [];

};

PlayerPositionValidator.prototype.addPosition = function(x, y){

var pos = {

x: x,

y: y

};

this.positions.push(pos);

};

PlayerPositionValidator.prototype.checkLast = function(x, y){

var pos = this.positions[this.positions.length - 1];

return Math.abs(pos.x - x) <= this.maxDx

&& Math.abs(pos.y - y) <= this.maxDy;

};

The above class keeps track of the maximum vertical and horizontal displacement that a player can possibly have in a frame (or however often the server validates a new user position). By associating an instance of it to a specific player, we can add new incoming positions as well as check it against the last one received to determine whether it's greater than the maximum possible displacement.

A more complicated case to check for and validate against would be making sure that the player does not report potentially expired events or attributes (such as temporary power ups, and so on), or invalid input state (for example, the player is already in the air, but is suddenly reporting that a jump was initiated).

To make matters even more complex, there is another case that we need to be aware of, which is very difficult to check. So far, as we've discussed, the solution to combat against players trying to manipulate game state is to use the authoritative server's power to overrule clients' actions. However, as we will discuss in the next section, there's one family of problems that even an authoritative server can't really prevent or recover from.

Artificial intelligence

It is one thing to detect a player who is trying to cheat because the reported moves are impossible to make (for example, moving too fast or firing a weapon that is not available in a given level in the game). An entirely different thing, however, is to try to detect a cheater because he or she is playing too well. This is a possible vulnerability that we can face if the vicious player is a bot playing perfect games against honest humans who are trying to compete.

The solution to this issue is as complex as the problem. Assuming that you want to prevent bots from competing against humans, how can you possibly determine that a series of inputs are coming from another software instead of a human player? Presumably, although every move will be a legal one, the level of accuracy will likely be orders of magnitude higher than everyone else's.

Unfortunately, implementations in code demonstrating ways to combat this class of problems is beyond the scope of this book. In general, you will want to use various heuristics to determine whether a series of moves is too perfect.

Building secure games and applications

Now that we've discussed some basic things to watch out for, and the things you shouldn't perform in your games; we will now take a look at some simple concepts that we cannot leave out of the game.

Again, most of these concepts apply to web development in general, so those of you coming from that world would feel right at home.

Authoritative server

Hopefully, it is clear by now that the key to having trustworthy information is to ensure that the source of that information is trustworthy. In our case, we rely on the game server to listen to all of the clients and then determine what is the truth about the current game state.

Should you ever find yourself in a situation where you are considering not using a server-client model for your multiplayer game in favor of some alternative format, one thing you should always keep in mind is that the security like that can be obtained by putting an authority between two players. Even if a single player decides to manipulate and cheat his or her own game client, the authoritative game server can ensure that other players still have an equal and fair experience.

While not every game format calls for an authoritative game server, you should have a really good reason for not using one when your specific game could be implemented using one.

Session-based gameplay

One of the benefits of modern browsers is that they feature very powerful JavaScript engines that enable us to do so much in the client with straight JavaScript. As a result, there is a lot of heavy lifting that we can offload from the server to the client.

For example, suppose we want to save the current player's game state. This would include the player's current position, health, lives, score, etc., as well as virtual currency, achievements, and more.

One approach would be to encode all of this information and store it in the user's machine. The problem with this is that the user could alter the saved file, and we would never know about it. Thus, a common step in this process is to create a hash of the final saved file, then later use this same hash to ensure that the game's saved file hasn't been altered.

Note

What is the difference between a hash and an encryption?

Perhaps, you have heard both the terms being used interchangeably, but they're actually very different concepts. While both are often associated with security, this is about the only similarity that they share.

A hash function maps a string of some arbitrary length to a string of some fixed length. Given the same input string, the same output hash is always returned. The main feature of a hash function is that the mapping is unidirectional, which means that the function cannot be reverted in order to get the original input from its output.

For example, the Rodrigo Silveira input string would map to something like 73cade4e8326. Hashing this output string would return something completely different from itself or the original input.

Encryption, on the other hand, is a way to convert some input string into a different representation of that string, but with the ability to reverse (or undo) the function and get back the original input string.

For example, if you use Caesar's cipher (named after the powerful Roman general, not the Brazilian soccer player) to encrypt the Rodrigo Silveira string with a shift value of 3 (which means that every character in the input text is shifted by 3 letters), you'd get the output as Urguljr Vloyhlud.—that is, the third character after the letter R, which is the letter U, and so on. If we apply a shift value of -3 to the output string would give us back the original string.

In short, for all practical purposes, a hash cannot be reversed while an encryption can.

However, if we also store the hash with the client, then all they would need to do after altering the game save file is to recalculate the hash, and we'd be right back where we started.

A better approach would be to calculate the hash on the server, store the hash in the server, and associate it with the player through some user account system. This way, if any tampering is done to the locally stored file, the server can verify it using the hash to which only it had access.

There are also cases when you might want to store API keys or other unique objects of this nature with the client. Again, the key principle here is that anything that touches the client is now under the control of your enemy and cannot be trusted.

Thus, the main takeaway from this section is to always store keys and other sensitive pieces of data like it inside the server and associate and proxy them to and for the player through session tokens.

Security through obscurity

While obscurity is not a form of security, it does add a layer of complexity that slows down the truly determined (and skilled) malicious user and filters out most other evil doers who would otherwise attempt to exploit your game.

In web development, the most common way to obscure your game is by running your final source code through some JavaScript compiler that safely renames variables and function names as well as rewrites code in a way that is equivalent to your original input code but performs the same tasks.

For example, you might have code like the following one, which can be exploited very easily by the player by changing the value of some variables using their browser's JavaScript console:

Gameloop.prototype.update = function(){

Players.forEach(function(player){

hero.bullets.filter(function(bullet){

if (player.intersects(bullet)) {

player.takeDamage(bullet.power);

hero.score += bullet.hp;

return false

}

return true;

});

});

// ...

};

We don't have to look too closely at the previous function to realize that only bullets that hit other players in this fictitious game gives damage to each player and increase our own score. Thus, writing a function to replace that is trivial or at least modifying its important parts to the same end can be just as easy.

Now, running that function through a tool such as Google's closure compiler (to know more on closure compiler, refer to https://developers.google.com/closure/compiler/) would output something similar to the following, which is clearly not impossible to manipulate but is certainly not as trivial:

_l.prototype.U=function(){c.forEach(function(e){i.a.filter(

function(o){return e.R(o)?(e.a4(o.d),i.$+=o.F,!1):!0})})};

Most JavaScript obfuscator programs will rename function names, variables, and attributes as well as remove unnecessary whitespace, brackets, and semicolons, making the output program very compact and hard to read. Some additional benefits of using these programs prior to deploying your code include having smaller files that you'll end up sending to your clients (thus saving bandwidth), and in the case of closure compiler, it rewrites parts of your code so that the output is optimal.

The key takeaway from this section is that adding layers of complexity to your code makes it that much more secure, and at the very least, helps you to get rid of some class of attackers. Much like adding a camera above your front door won't necessarily eliminate possible intruders from breaking in, it sure does go a long ways in scaring unwelcome visitors.

"Remember, however, that obscurity is not security at all. It is trivial to deobfuscate an obfuscated JavaScript program (even compiled programs can be easily decompiled back into partial source code). You should never rely on obfuscation and obscurity alone as a solid form of security. Obfuscating your deployed application should be a final touch to an already secure system, especially considering the major benefits of obfuscation, as mentioned previously.

Reinventing the wheel

Like most problems in computer science, someone has already found a solution and converted it into code. In this regard, we have been particularly benefited by so many generous (and very smart) programmers who distribute their solutions through open source projects.

In this section, I invite you to look for existing solutions instead of taking the time to write your own. Although coding complex solutions to interesting problems is always fun (unless, maybe, your boss is pressing you about an upcoming deadline), you may find that your efforts are better invested in making your actual game.

As we've discussed in Chapter 2, Setting Up the Environment, having access to the Node.js ecosystem gives you a lot of leverage to find, use, and eventually share great tools for many problems that you may come across when you develop your games.

In sticking with the theme of security and fair play, what follows is a list of common tools that we can use through Npm and Bower (as well as Grunt and Gulp) to help us deal with security in our games.

Npm install validator

This module allows you to validate and sanitize data very effortlessly. You can use validator on the server as well as in the browser. Simply require the module in and call its various methods on your input:

var validator = require('validator');

validator.isEmail('foo@bar.com'); //=> true

validator.isBase64(inStr);

validator.isHexColor(inStr);

validator.isJSON(inStr);

There are methods for checking just about any type of data or format as well as to sanitize data so that you don't have to write your own functions for it.

Npm install js-sha512

This simple module is used to hash strings using a variety of algorithms. To use the library as a standalone library in the browser, you can also import it using Bower:

bower install js-sha512

To use js-sha512, simply require it to the desired hashing function and send it the string to be hashed:

sha512 = require('js-sha512').sha512;

sha384 = require('js-sha512').sha384;

var s512 = sha512('Rodrigo Silveira');

var s384 = sha384('Rodrigo Silveira');

Npm install closure compiler

As mentioned previously, Google's closure compiler is a very powerful software that was open-sourced several years ago. The benefits that can be gained by using the compiler extend far beyond simply wanting to obfuscate code. For example, the compiler allows you to annotate your JavaScript code with data types, which the compiler can then look at and tell you whether a variable violates that contract:

/**

* @param {HTMLImageElement} img

* @constructor

*/

var Sprite = function(img) {

this.img = img;

};

/**

* @param {CanvasRenderingContext2D} ctx

*/

Sprite.prototype.draw = function(ctx) {

// ...

};

/**

* @param {number} x

* @param {number} y

* @param {Sprite} sprite

* @constructor

*/

var Player = function(x, y, sprite) {

this.x = x;

this.y = y;

this.sprite = sprite;

};

In the given sample code , you will note that the Player and Sprite constructor functions are annotated with the @constructor annotation. When the closure compiler sees the code calling these functions without a new operator, it will deduce that the code is being exercised in a way different from how it was intended and raise a compilation error so that you can fix the bad code. In addition, if you attempt to instantiate a Player, for example, and the value sent to the constructor is not a pair of numbers followed by an instance of the Sprite class, the compiler will bring this to your attention so that your code can be corrected.

The easiest way to use the closure compiler is to lean on Grunt or Gulp and install the equivalent build task for closure. The popular solutions are as follows:

// For Grunt users:

npm install grunt-closure-compiler

// If you prefer Gulp:

npm install gulp-closure-compiler

Fair play and the user experience

So far in this chapter, we have discussed many different aspects of security, all of which were aimed at providing fair play to your users. Although we can do the best we can to secure our servers, intellectual property, user data, and other players, at the end of the day, the attacker will always be at an advantage.

Especially in multiplayer games, where dozens, if not hundreds or thousands of different players will be enjoying your game at the same time, you may come to a point where attempting to secure a player against himself is not a good investment of time or other resources. For example, if an isolated player wishes to cheat his or her way into jumping higher than the game allows or change a save game in order to reflect extra lives, then you may be better off just letting that player proceed with the hack on his or her own client. Just be absolutely sure that none of the other players are affected by it.

The key takeaway from this section as well as from the entire chapter is that user experience is the king. Even more so when multiple players are sharing a game world looking for a fun time and one of these players is only looking for a way to ruin it for everyone; you must make sure that no matter what happens, the other players can continue to play.

Summary

With this chapter, we wrapped up the discussion on multiplayer game development although it covers a topic that must be a deep part of your game development from the beginning. Remember that security cannot simply be added at the end of the project; instead, it must be built in deliberately with the rest of the software.

We saw some of the most basic security vulnerabilities in browser-based games as well as common ways to protect your games against them. We also talked about a few techniques that no serious game should be built without. Finally, we looked at how to implement some of these techniques using existing open source tools through Node's Npm.

In conclusion, now that you have cleared the last level of this journey of learning the basics of multiplayer game development in JavaScript, I want you to know that as exciting as this might have been, your journey is not quite over yet. Thank you for reading, but the princess is in another castle! You must now get busy with writing the next multiplayer game that will take all players through a journey full of fun, entertainment, and real-time awesomeness. Game over!