Husky
Husky

Reputation: 6186

Prevent malicious users from abusing and spamming unauthenticated open APIs

Here's a security problem I've encountered a couple of times when building small web-based projects interacting with a REST API service. For example, let's say you're building a casual JavaScript-based game where you want a leaderboard of highscores, so you need to post the scores of users to a database.

The easiest solution would be to build a simple web service, e.g. using PHP, Node.js or Python, that accepts GET request and saves the results to a database. Let's imagine the API looks something like this:

GET https://www.example.com/api/highscore?name=SuperGoat31&score=500

Creating such an API for posting highscores has some obvious drawbacks. A malicious user could write a three-line piece of PHP code to spam the database full of false results, for example:

for ($i = 0; $i < 100; i++) {
    file_get_contents("https://www.example.com/api/highscore?name=SuperGoat31&score=5000000");
}

So, I'm looking for a way to prevent that. This mostly relates to small hobby or hackathon projects that just need some kind of protection that will prevent the most obvious of attacks, not large enterprise applications that need strict security. A couple of things I could think of:

1. Some form of authentication

An obvious way to solve this would be to have user accounts and only allow requests from logged-in users. This unfortunately has the drawback of putting up a large barrier for users, who need to get an account first. It would also require building a whole authentication workflow with password recovery and properly encrypting passwords and the like.

2. One-time token based protection

Generate a token on the server side and serve that to the user on first load, then only allow requests that serve that specific token. Simple enough, but also very easy to circumvent by finding the requests in a browser web inspector and using that for the three-line PHP script.

3. Log IP address's and ban when malicious use happens

This could work, but I feel it's not very privacy friendly. Also, logging IP addresses would require GDPR consent from users in Europe. Also doesn't prevent the actual spamming itself so you might to first clean up the mess before you start banning IP addresses.

4. Use an external service

There are services that provide solutions to this problem. For example, in the past I've used Google's reCAPTCHA to prevent malicious use. But that also means integrating an external service, making sure you keep it up to date, concerns about the privacy aspects (esp. regarding a service like reCAPTCHA), etc. It feels a bit much for a weekend project.

5. Throttle requests

I feel this is probably the easiest solution that actually works for a bit. This does require some form of IP address logging (which might give the problems stated in 3), but at least you can delete those IP addresses pretty quickly afterwards.

But I'm sure there are other methods I've missed, so I would be curious to see other ways of tackling this problem.

Upvotes: 5

Views: 2994

Answers (6)

Nik
Nik

Reputation: 689

Taking into account all mentioned limitations, I would recommend using a combination of methods:

  1. Simple session authentication based on one-time token
  2. Script obfuscation
  3. Request encryption with integrity control

Example:

let req_obj = {
  user: 'SuperGoat31',
  score: 123456,
  sessionId: '4d2NhIgMWDuzarfAY0qT3g8U2ax4HCo7',
};
req_obj.hash = someCustomHashFunc(JSON.stringify(req_obj));
// now, req_obj.hash = "y0UXBY0rYkxMrJJPdoSgypd"

let req_string = "https://www.example.com/api/cmd?name=" +
                 req_obj.user +
                 "&data=" +
                 Buffer.from(JSON.stringify(req_obj)).toString('base64');

// now, your requests will look like that:
"https://www.example.com/api/cmd?name=SuperGoat31&data=eyJ1c2VyIjoiU3VwZXJHb2F0MzEiLCJzY29yZSI6MTIzNDU2LCJzZXNzaW9uSWQiOiI0ZDJOaElnTVdEdXphcmZBWTBxVDNnOFUyYXg0SENvNyIsImhhc2giOiJ5MFVYQlkwcllreE1ySkpQZG9TZ3lwZCJ9"

For casual players, this allows start playing very quickly, as no explicit registration is required. Upon generation, token might be saved as cookie for repetitive use, but this is not necessary, single-time use would also suffice. No personal info gathered.

However, if short-term storage of some client information is an option, the token might be not just some random bytes, but an encrypted string, containing some parameters, such as random salt + IP address + nickname + agent id + etc. In this case you may start silently ignore certain requests from fraudulent clients upon detection.

Obviously, this would be very easy to crack for a professional, but this is not our goal. When such simple methods are mixed with several kilobytes of logic of the game and obfuscated, figuring out how to deal with it would require significant amount of knowledge and time, which might serve as a sufficient barrier.

As it is all about balance between convenience and protection, you may implement some additional scoring logic to detect cheating attempts, like final score cannot end with '0', or cannot be even, etc. This would allow you to count cheating attempts (in addition to counting forged requests) and then estimate efficiency of implemented combination of methods.

Upvotes: 5

Jim
Jim

Reputation: 21

I suspect that a perfect solution will be elusive because two of your wishes are, perhaps, contradictory:

"You need to post the scores of users to a database" but... "prevent the most obvious of attacks" without "Some form of authentication."

The most obvious of attacks are those from users without some form of authentication.

You wish this system to work without placing an undue burden on your users. You wish to avoid the usual login and password authentication which can be cumbersome for users.

I think there is a way to accomplish what you want by creating a very simple form of authentication by the use of a one-time token based protection. And I would also incorporate IP tracking against abuse. In other words, let's combine your options 1 and 2 and 3 in the following way.

You already have implied that you will maintain a database, and that within the database, user names will be unique (otherwise you couldn't record unique high scores). Let people sign up freely by submitting their requested user name, which you'll accept if not already used by someone prior. Track the sign-up requests by IP address to detect and prevent abuse: too many sign-ups from one IP address within a given timeframe. So far, the burden is all at the server end, not on the user.

When you process a valid sign-up (i.e. new user name) into the database, you will also generate, record into the database, and return to the user a shared secret (a token) that will be used by the Time-based One-time Password (TOTP) algorithm.

Don't reinvent this.

See:

When you return a token to the user, it will be in the form of a "QR Code"

QR code

which the user will scan and store with his "Google Authenticator" or equivalent TOTP application.

When the user returns to your web site to update his high score, he will authenticate himself using his Google Authenticator" or equivalent TOTP application. These are often used for "second factor" authentication, 2FA (Multi-factor authentication), but because of your need for less strict security, you'll be using the TOTP authentication as the primary and only form of authentication.

So we have combined a form of authentication which doesn't place a very high burden on the user (apps already widely available and in use), with one-time token based protection (provided by the TOTP app) and a little bit of IP address-based abuse protection for the initial sign-ups.

On of the weaknesses of my proposal is that a user may share his TOTP token with another person, who may then impersonate him. But this is no different from the risk of password sharing. And there will be no "recover my lost password" option.

Upvotes: 2

user13152486
user13152486

Reputation:

I would suggest using reCAPTCHA V2.

Admittedly, v3 provides better protection, but it is hard to implement, so go with v2.

Come on, it is just a few lines of code.

How it should work (according to me):

  • You are at the main page willing to play the game
  • You solve the reCAPTCHA
  • Then the app sends a one-time token with a script tag which establishes a websocket request with your server (using socket.io) with the one-time token and then it is destroyed immediately (from the server as well as the client) after establishment of a connection
  • Your server validates the token and accepts the request of websocket and then it will send the HTML content
  • Just create a div and set the value using obj.innerHTML
  • You can use styles in body (I guess)

And the most important point is obfuscating your code.

Security

  • Websockets are harder to reverse engineer in a test environment
  • Even if they create a web socket, it won't respond, because they don't know the one-time token
  • It prevents script blocking (as the script loads everything on the page)
  • It provides real-time communication
  • The only way out is to somehow get your hands on Google's reCAPTCHA token which is impossible, because it means going against Google
  • You can’t reuse any token (however immediate it be), because it was destroyed from both the sides
  • One more last tip: set a timeout for the one-time token to about 15 seconds
  • How will it help? It will prevent someone (extremely malicious) from pausing the Chrome debugger and get the token and put it in their stuff as 15 seconds is ok for slow networks also, but not a human

Upvotes: 1

jperry1147
jperry1147

Reputation: 276

I would recommend a mix of code obfuscation and using web sockets to request the score, rather than post the score. Something like socket.io (https://socket.io/) where the server sends a request with a code in it and your game responds with the score and that code changed in some way.

Obviously a hacker could look through your code for how your game responds to requests and rewrite it, which is where the obfuscation is important, but it does at least hide the obvious network traffic and prevents them posting scores whenever they feel like it.

Upvotes: 1

JohnFF
JohnFF

Reputation: 731

I would tackle this in a slightly different way: usernames/gamertags. Depending on how frequently you find gamertags and usernames sharing the same IP. So if you only accept a maximum of, say, 5 gamertags per IP, and you also throttle the frequency of updates per gamertag, you have a fairly spam-resistant system.

Upvotes: 1

Evert
Evert

Reputation: 99533

Your list of solutions are mostly mitigations, and they are good ideas if they are your only tools. The list seems pretty exhaustive.

2 major ways to actually solve this problem are:

  1. Remove the incentive of cheating. There's no point submitting a fake score if you are the only person who can see the score. Think about the purpose of why you even want a global high-score list. Maybe there's another way you can reach your objective that makes it uninteresting (or undesirable) to cheat.
  2. Have the server completely manage (or duplicate) the game state. You can't cheat if the server calculates the score. For example, if you're modelling a chess game the server can compute every valid move, preventing clients from submitting moves that wouldn't be possible.

It's possible that for your specific case neither are possible, but if you can't adopt either of these strategies you are stuck to imperfect detection mechanisms.

Upvotes: 3

Related Questions