Nodejs Security issue client sending request

I’m creating a browser game, I know browsers aren’t really safe but I was wondering if there’s any workaround my issue.

A player kills a monster and the platform sends an ID to my backend like so:

Axios({
   url: this.server + "reward",
   headers: { token: "foo123", charToken: "bar123" },
   method: "POST",
   data: {
      id: 10001, // Monster ID
      value: 10 // How many monsters were killed
   },
});

The problem is, I see no possible way to prevent a user to just send random requests to the server, saying he actually did this level 300 times in a second and getting 300x reward items/exp.

I thought about requesting a token before sending this reward request, but this just makes it harder and doesn’t really solve anything.

15 thoughts on “Nodejs Security issue client sending request”

  1. Your current design

    As it currently stands, the design you’ve implied here won’t be manageable to mitigate your concerns of a replay attack on your game. A helpful way to think about this is to frame it in the context of a "conversational" HTTP request/response paradigm. Currently, what you’re allowing is something akin to:

    • Your client tells your server, "Hey, I’ve earned this reward!"
    • Your server, having no way to verify this, responds: "Great! I’ve added that reward to your account."

    As you guessed, this paradigm doesn’t provide very much protection against even a slightly motivated attacker.


    Target design

    What your design should enforce is something more along the lines of the following:

    • Your clients should only be able to ask the server for something. ("I’ve chosen to attack monster #123, what was the result?")
    • Your server should respond to that request with the information pertinent to the result of that ask.
      • "You’re not in range of that enemy; you can’t attack them."
      • "That enemy was already dispatched by you."
      • "That enemy was already dispatched by another player previously."
      • "You can’t attack another enemy while you’re in combat with another."
      • "You missed, and the enemy still has 100hp. The enemy struck back and hit you; you now have 90hp."
      • "You hit the enemy, and the enemy now has 1hp. The enemy struck back and missed; you still have 100hp."
      • "You hit the enemy, and the enemy now has 0hp. You’ve earned 5 coins as a reward."

    In this design, your server would be the gatekeeper to all this information, and the data in particular that attackers would seek to modify. Instead of implicitly trusting that the client hasn’t been modified, your server would be calculating all this on its own and simply sending the results of those calculations back to the client for display after recording it in a database or other storage medium.

    Other items for your consideration

    1. Sequential identifiers

      While your server might still coordinate all these actions and calculate the results of said actions itself, it’s still possible that any sufficiently motivated attacker could still find ways to exploit your backend, as you’ve used predictably-incremented values to identify your back-end entities. A small amount of study of your endpoints along with a short script could still successfully yield unintended in-game riches.

      When creating entities that you’d prefer players not be able to enumerate (read: it probably is all of them), you can use something akin to a UUID to generate difficult-to-predict unique identifiers. Instead of one enemy being #3130, and the next one being #3131, your enemies are now known internally in your application as 5e03e6b9-1ec2-45f6-927d-794e13d9fa82 and 31e95709-2187-4b02-a521-23b874e10a03. While these aren’t, by mathematical definition, reliably cryptographically secure, this makes guessing the identifiers themselves several orders of magnitude more difficult than sequential integers.

      I generally allow my database to generate UUIDs automatically for every entity I create so I don’t have to think about it in my backend implementation; support for this out of the box will be dependent on the RDBMS you’ve chosen. As an example in SQL Server, you’d have your id field set to the type UNIQUEIDENTIFIER and have a default value of NEWID().

      If you do choose to generate these for whatever reason in Node.js (as you’ve tagged), something like uuidjs/uuid can generate these for you.

    2. Rate limiting

      You haven’t mentioned anything about it in your question (whether you’ve already used it or not), but you really should be enforcing a rate limit for users of your endpoints that’s reasonable for your game. Is it really plausible that your user JoeHacker could attack 15 times in the span of a second? No, probably not.

    Reply

Leave a Comment