Skip to content

Instantly share code, notes, and snippets.

@seiyria
Last active June 18, 2022 18:00
Show Gist options
  • Save seiyria/1bfd939a1d0566223228 to your computer and use it in GitHub Desktop.
Save seiyria/1bfd939a1d0566223228 to your computer and use it in GitHub Desktop.
Common Pitfalls in JS-based Games

update: this post has been moved to my blog

Welcome! You might be reading this out of curiosity, or because you want to improve your programming capabilities to stop people from exploiting your JS games. Given that the first thing I do when I open a new incremental is open the terminal and start messing around with your games, I figured it's about time to write something about what I see and how I break your games. Consequently, I'll describe ways you can protect your games from the basic code manipulations I perform. Some might say "you're just ruining the game for yourself!" while I'm going to turn around and say "I don't care" -- that's not the point of this!

NB: This will only apply to vanilla JS applications, which I see more commonly. Frameworks like AngularJS and such are out of scope for this post. Advanced techniques such as using a debugger, while slightly more on topic, will also be disregarded for now.

Lets talk about me for a second: I'm seiyria and I'm a professional mostly-JavaScript software developer. That is to say, I really like JavaScript -- the ecosystem, the language, and the reach of JavaScript (did you know, there's hardware powered by JavaScript? Awesome!). I've been programming for 10+ years now, sometimes I make applications, sometimes I make games -- a little bit of everything.

Okay, enough about me, lets go into the things you'll care about.

First, I'd like to talk about some basic client-side things that can be exploited. Later, I'll get into protecting your server (as much as you can, anyway). It's good to note that your game, if it's client side only, can never be fully protected. You can take measures to do so, but there are more techniques not listed here that can be performed.

"Clean Code"

Clean code, or non-obfuscated code, is something that makes it very easy to logically see how your code works. This means that you're basically uploading your code as you wrote it with no manipulation. It's also very easily preventable. It won't stop someone who's determined to push through but 50% of the time I'll look at it and go "eh, too much work for me to care."

Essentially, what you should do here is just run your code through UglifyJS once before pushing it to a server. There are three benefits: A smaller JS footprint, less HTTP requests (assuming you concatenate them all into one file), and you've performed a small amount of mitigation, yay!

Public Functions

If all of your functions are public, ie, they look like this:

function myPublicFunction() {
  // perform awesomeness
}

or this:

var myPublicGame = {
  doSomethingAwesome: function() {
    // awesome level > 9000
  }
};

Then we have a problem. Lets suppose your "do something awesome" functions are game-critical functions like "level up" or "add resources" -- it's very trivial for me to open the console and go myPublicFunction() or myPublicGame.doSomethingAwesome(). What's even better, is if your function accepts an argument for how much I get to increase my resources or level by. My favorite.

What can you do to fix this? Two simple ways; the first requires no real planning on your part, just put this around all of your code:

(function() {
  //your code here
})();

What does that do? Well, it puts all of your code in an isolated scope that can't be accessed globally. This means that all of your code is inaccessible via the terminal, for the most part. The second way requires a bit more thought, but it lends itself better to an application design standpoint. JavaScript has a class-like syntax. I say class-like, because JS does not have traditional classes (not yet, anyway). Lets look at that syntax here:

var MyClass = function() {
  var myPrivateVariable = 4;
  
  var self = this;
  self.myPublicVariable = 10;
};

var myClassInst = new MyClass();

console.log(myClassInst.myPrivateVariable); // => undefined
console.log(myClassInst.myPublicVariable); // => 10

As you see here, you can fake private variables by limiting the scope they're accessible from. Why does this matter? Well, lets make it a more practical example:

var MyGame = function() {
  var growthRate = 100;
  var myCurrency = 0;
  
  var grow = function() {
    myCurrency += growthRate;
  };
  
  setInterval(grow, 1000); // grow more every second of my life!
};

If you've been following along, you'll notice that I can no longer go into the console and type MyGame.growthRate = 10000000000 or for(var i=0; i<10000; i++) { MyGame.grow(); } -- it's all private! There are a few more approaches here that could be taken, such as using RequireJS or other tools to manage your files, but lets keep it simple for now.

Some games that have exploits like this available:

  • BlackMarket -- devMode(2) gives you 100 quintillon money. More succinctly, they expose money and prestige as global objects, which can be freely manipulated. There's also a cheats.js file. If you're using grunt or gulp or some build system, this should be excluded from your distribution build for sure.
  • Meme Clicker -- app.memes = 1000000000000000000
  • A Dark Room -- literally everything is exposed. The game is more complex than "get currency, spend currency" though, so I'll leave this one as an exercise to the reader.
  • Many more games; I'm not going to go make a huge list, I'm just providing some small examples.

LocalStorage modification

Maybe a little overkill, but if your game stores things in localStorage, it's probably vulnerable. Most games I see just store a simple hash object with some data, or store a bunch of keys with data. Suppose you've protected your game via the above measures and now you want to make sure everything is good to go. Lets look at Meme Clickers data (here is an example). All I have to do is modify the memes attribute to be whatever I want, reload the page, and it'll be peachy - the game won't even know I messed with it; for all it knows, that's a valid state. Similar case with Blackmarket -- check this out.

"Alright," you say, "what can I do about that?" Simple. Store a hash of all the data. MD5, SHA-1/2, anything. If you hash all of the data you're saving, store the hash with it, and then load the game, all you have to do is verify the hash upon loading the game. If the hash is invalid, the save is invalid, and should be treated as a fresh start.

Server Exploits

Okay, so this is what prompted this article. Recently there was a game introduced called IncrementalGame. It's pretty meta, and it's also backed by a server. That last bit is what makes it a much more fun target than other games, since other people can see what I'm doing, too. Yesterday, I posted a simple exploit that allowed anyone to massively increase the votes behind any game listed there. I simply dug around in the code until I came across something that looked like it did something, watched my Network tab in my dev tools, and figured out how the game worked. Here are some things to note about having a game with a server:

  • Always validate the data coming in. Yesterday, I was able to send negative votes to any set of games simply by using my exploit, but changing the sign on the 10 to be -10. In some games, it may make sense to allow positive and negative inputs, but this one is not one of those cases. The resolution here, always validate the data coming in from the client.
  • If your API is entirely internal, giving back error messages like "vote size > 20; truncating to 20" just makes it so I know that I can't send a value greater than 20, which means I can send a larger request. The resolution here, don't send error messages that don't need to be sent.
  • If your server processes a lot of data, it's much easier to DoS it. In this case, IncrementalGame.com took an array of votes and processed every one of them. Supposing that I put 10000 individual votes into the array being sent to the server, I can make the server choke when it has to process all of that data repeatedly. The resolution here, simplify the data going to your server.
  • If your game needs to enforce a rate limit on how much you can interact with it, say, you can only vote 1.3x / second, then you better not be attempting to enforce that on the client. As shown previously, I can simply make an array of 10000 votes and send that to the server, raw, without clicking any buttons on the page. If disabling buttons on the page is the only thing stopping people from spamming the server, that can only end poorly. The resolution here, make sure your server is effectively rate-limiting your players from spamming it too much. This is not to say that you should only validate on your server, but your server should be authoritative! You should still validate on the client side.

Conclusion

In short, it's very easy to make a game that's exploitable. Hopefully the techniques listed above not only help you grow as developers, but make your game and have it played the way you intended.

If not, I'll be there to break it.

Want me to take a look at your game / app? Send me a message!

@ClayTaeto
Copy link

@birjj
Copy link

birjj commented Apr 23, 2015

@Sdonai: Since you're using Angular, you aren't really able to do the same scope-binding that is suggested in this gist. While it's more cumbersome to (manually) manipulate data in an Angular model, there really isn't anything stopping you from doing so. Not that it matters much; since no one else will see your score, you're only really cheating yourself by changing the values.

The one thing you can change, is to move sensitive functions such as candyPerson.produce and candyPerson.getCost from the instance to the scope, so they can't be easily overwritten/called. This messes with testability, though.

@seiyria
Copy link
Author

seiyria commented Apr 24, 2015

Yeah. I specifically avoided angular for this post because it would have gotten way longer. It's trivial to jump into an angular scope and modify data. Some games attempt to sidestep that problem by setting data in a timer (thus, it would overwrite any modified data when the timer ticks).

@wjmao88
Copy link

wjmao88 commented Apr 24, 2015

Client side JS is inherently insecure. No matter how well you try to hide things, the browser is on the user's side to make everything more transparent to them, for very good reasons.

To get access to variables inside a function's scope, simply open the source file and put a break point on that line of code, and when the browser pauses at the line, you have access to everything in that function scope.

Code uglifiers usually only uglify variables within a single function scope as they are defined, not properties because there it is hard if not impossible to syntactically analyze which object instance would be which across many functional scopes.

Also, even if you are able to obfuscate code perfectly, it still have to interact with the framework if you are using one, with likely jQuery, and at least the browser, which all have known and largely unchangeable API.

Even if you manage to somehow obfuscate that, which I have never encountered, I can monitor events by looking at event listeners on DOM elements using developer tools and start tracing from there.

So I'd say accept the fact that it is always possible to hack any part of your game that is on the front end and, as this gist has pointed out, always validate on the back end and have the sever be the only reliable source of truth.

@maxencefrenette
Copy link

Really nice job putting the basic ways javascript games are hacked. I'd like to add another exploit I like to use and that you didn't talk about. Often, i'm able to write a script that clicks 1000 times per second on the "Get money" button. There should always be a timer that prevents the button being clicked more than a certain amount of time per second.

Another hack that i like to do is to globally redefine setTimeout and/or setInterval to make the game go faster. For those who are curious, here is a code example that would make the game go 1000 times faster.

(function(){
  var oldSetTimeout = window.setTimeout;
  window.setTimeout = function(cb, delay) {
    return oldSetTimeout(cb, delay/1000)
  }
})()

An easy way to prevent this is check how much time has actually passed between frames.

@AkumaNoTsubasa
Copy link

I'm not good with JS, so I can't go into that. But the server-side stuff is something that is the very first thing you do, when recieving stuff from a user.. validate it's not crap-shit or even intrusion stuff o.o
People really go without securing themself ?

@SixBytesUnder
Copy link

Meme Clicker creator here.
Sorry to ruin your examples, I just applied some of your advices to my code :)
You can't do app.memes = 1000000000000000000 any more.
Thank you for this.

@jarcane
Copy link

jarcane commented Apr 27, 2015

Perhaps this is one advantage to having done mine in compiled ClojureScript?

@seiyria
Copy link
Author

seiyria commented Jun 16, 2015

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment