Skip to main content
BlogComputeDefending Against a Login API Brute Force Attack

Defending Against a Login API Brute Force Attack

A red block symbol with the following text: "Defending Against a Login API Brute Force Attack: Because cutting corners can cripple you"

Let’s face it, when you’re rushing to meet a big release deadline, you cut corners. Your test coverage gets spotty, your code isn’t so DRY, and your exception handling is sent to the tech debt graveyard—I mean, backlog. We’ve all been there.

But when it comes time to cut corners, do not cut out implementing the “maximum failed login attempt” safeguard. If your login API doesn’t have proper safeguards in place, then gaining access to a user account by brute force is relatively easy to pull off nowadays.

In this post, we’ll show you just how an attacker might brute force a login API. Then, we’ll discuss countermeasures you can put in place to defend your systems.

An example login API

To demonstrate, we’ve built a very basic API server using Node.js and Express. It listens for POST requests to a single endpoint, /login. The request body is expected to have a username and a password. When successful credentials are provided, the API returns 200 OK. Otherwise, it returns 401 UNAUTHORIZED.

Here’s the code for our simple Express server:

const express = require('express');
const users = require('./users.json');

const app = express();
const PORT = 3000;

// Middleware to parse JSON bodies
app.use(express.json());

app.post('/login', (req, res) => {
    const { username, password } = req.body;

    // Check if the username exists and the password matches
    if (users[username] && users[username] === password) {
        return res.status(200).send('OK');
    } else {
        return res.status(401).send('UNAUTHORIZED');
    }
});

// Start the server
app.listen(PORT, () => {
    console.log(`Server is listening on port ${PORT}`);
});

For this simple demo, we keep the list of usernames and passwords in a file called users.json.

We’ve deployed our server on a Linode, listening on port 3000. Let’s try out a request:

~$ curl -i \        -X POST \        --header "Content-type:application/json" \        --data '{"username":"user08","password":"testpassword"}' \         172.233.153.34:3000/login

HTTP/1.1 401 UnauthorizedX-Powered-By: ExpressContent-Type: text/html; charset=utf-8Content-Length: 12ETag: W/"c-MvHJP5yPj49I/9tX+wGrvHWbTRk"Date: Sat, 25 May 2024 19:17:34 GMTConnection: keep-aliveKeep-Alive: timeout=5
UNAUTHORIZED

Our login API is up and running!

Now, if we wanted to brute force our API, we might write a script like this:

#!/bin/sh

API_ENDPOINT="http://172.233.153.34:3000/login"

USERNAME="user5"

echo "Attempting to login at $API_ENDPOINT as \"$USERNAME\""

for i in {1..8}
do
  PASS="password${i}"
  RESULT=$(curl --silent -X POST --header "Content-type:application/json" --data "{\"username\":\"$USERNAME\",\"password\":\"$PASS\"}" $API_ENDPOINT)
  echo "\"$PASS\": $RESULT"

  if [[ $RESULT == 'OK' ]]; then
    echo "!!! Password for \"$USERNAME\" found: \"$PASS\""
    break
  else
    sleep 1
  fi
done

When we run our rudimentary brute force script, it looks like this:

$ source bruteforce.sh
Attempting to login at http://172.233.153.34:3000/login as "user5"
"password1": UNAUTHORIZED
"password2": UNAUTHORIZED
"password3": UNAUTHORIZED
"password4": UNAUTHORIZED
"password5": OK
!!! Password for user5 found: "password5"

Can an attacker really guess a password?

So, we’ve seen how easy it is to write a script which can at least cycle through some passwords for a given username. You might say, “Ok, that’s an interesting contrived example. But can an attacker really guess a password?

Here’s a list of the 10,000 most common passwords. That’s a pretty good start for any attacker. Think about it—have you ever come across somebody who uses “password123” or “qwerty”? It might even be you!

If an attacker knew a few usernames for your system, and they ran a script to loop through these common passwords, they might get a hit.

With every allowed attempt of a username and password combination, the chances of breaching an account increase.

This is a classic case of broken authentication, which is #2 on the OWASP Top 10 API Security Risks. If your application doesn’t properly protect against automated attacks, you’re asking for trouble. Without safeguards, your users’ accounts are at significant risk.

Passwords are often the weakest link in a system’s security:

  • Users reuse passwords across multiple sites.
  • Users choose easy-to-remember (and easy-to-guess) passwords.
  • Users rarely update their passwords.

All of these factors make brute force attacks frighteningly effective.

So let’s get back to our question: Can an attacker really guess a password? Absolutely. And if you’re not taking the right precautions, it could happen sooner than you think.

How do I protect my API?

There are multiple ways to defend against a brute force attack on your login API.

Establish a number of maximum login failed attempts

Set a limit on the number of failed login attempts that you’ll allow for each user. As login attempts for a user occur, keep a running count of login fails. If you reach that limit, lock the account temporarily or block subsequent requests from the sender’s IP address. This makes it much harder for an attacker to brute force their way in.

Use a web application firewall (WAF)

A WAF can help protect your API by detecting and blocking malicious activity.

  • Bot activity: A good WAF can distinguish between legitimate users and bots, blocking automated brute force attacks.
  • Suspicious login requests from behind Tor: Many attackers use the Tor network to hide their identity. Blocking or challenging requests from Tor nodes can reduce the risk of attacks.

Haltdos, which is a WAF available on the Linode Marketplace, offers these protections and more. By integrating such tools, you can significantly bolster your API’s defenses.

Implement rate limiting

Limit the number of API requests from a single IP address within a given time frame. This slows down brute force attacks and makes them less feasible. Rate limiting is a good practice for all your APIs and endpoints, not just your login API.

Enable multi-factor authentication (MFA)

If you add an extra layer of security, like MFA, then you can thwart brute force attacks even when the attacker guesses the password correctly. Successfully getting through the entire authentication flow requires something the user knows (password) and something they have (a phone or hardware token).

Monitor and analyze login attempts

Set up an API request monitoring solution to keep an eye on login attempts. Look for patterns that might indicate an attack. This can help you respond quickly to suspicious activity.

Implementing these measures can help protect your API from brute force attacks and keep your users’ accounts secure.

Conclusion

Protecting your login API from brute force attacks is crucial. Guessing a password is not as difficult as you might think, and automating guesses is child’s play. Is your system vulnerable? We’ve covered several things you can do to shore up your systems. These steps will significantly enhance your security posture.

In the frantic rush of building your applications and APIs, we’ll forgive you if you cut corners here and there. Everybody does it. But don’t leave your login API unprotected! Don’t wait until an attack happens—take action now to protect your system and your users. For more detailed guidance on logging and system monitoring, check out the Linode docs. If you plan to use Apache to serve your applications, you can check out this guide on configuring mod_evasive to help your server survive DoS/DDoS attacks.

Be safe!

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *