0

What's the best practice to secure a user login against brute force in PHP?

I like the idea to use a recaptcha to prevent automatic logins. This will cause high costs for the attacker. Also the attacker can't DoS user accounts by intentional using the wrong password.

Is this enough protection? Is it recommend to add a "x minutes" block after "y tries" anyway? Are there options to protect the login?

I know about 2FA*. But this should be an optional feature and I want to protect users that don't use 2FA too

* two factor authentication

Can O' Spam
  • 2,718
  • 4
  • 19
  • 45
za3223340
  • 111
  • 2
  • 7
  • 1
    This is a can of worms - extremely broad. As to "what's the best practice to secure a user login against brute force in PHP", people have many opinions on what's best, this I'm afraid, is question is really not useful, that doesn't mean it isn't a good question, just attracts too much unwanted opinion and argument – Can O' Spam Jun 27 '18 at 11:01
  • 1
    As @SamSwift웃 said, this isn't a really good question. That said, take a looky here as OWASP is pretty much the de facto authority on web security: https://www.owasp.org/index.php/Blocking_Brute_Force_Attacks – Loek Jun 27 '18 at 11:03
  • That's because this isn't so much a programming question as a server/software configuration question. Design patterns are a good idea 90% of the time. For protecting your website, every use case is different. Where Google absolutely needs captcha's, 2fa, throttling, IP blocking, etc; your local football club doesn't. Then throttling can be more than enough. Figure out what works best for your use case or consult experts. – Loek Jun 27 '18 at 11:06
  • @za3223340 - exactly, best practice tends to vary from company to company and person to person, there is no "common law of best practice" so to speak. We each have different ideas about what is the best form of security, be it 2FA, security by obscurity, mass encryption - anything really - it's far too broad to be useful to ask this here, it's best to do some (perhaps more?) google-fu in order to get an idea of what's around and get something that works best for you and your needs – Can O' Spam Jun 27 '18 at 11:24
  • Take a look at https://stackoverflow.com/questions/2090910/how-can-i-throttle-user-login-attempts-in-php and https://stackoverflow.com/questions/479233/what-is-the-best-distributed-brute-force-countermeasure – HTMHell Jun 27 '18 at 14:51
  • Link to owasp.org does not work any more. Altneratives: https://owasp.org/www-community/attacks/Brute_force_attack, https://wiki.owasp.org/index.php/Blocking_Brute_Force_Attacks – jm009 Jul 20 '22 at 01:56

1 Answers1

2

Progressive delays are usually the best security-usability trade-off.

In Airship, we implemented a progressive delay here (relevant config) where failed attempts from a specific IP subnet or towards a specific user account would increase the amount of time they must wait before successive attempts.

If you're looking for reusable code to get a subnet from an IP address:

<?php
declare(strict_types=1);
class StackOverflowCopyPaste
{
    /** @var int $v4MaskBits */
    private $v4MaskBits;

    /** @var int $v6MaskBits */
    private $v6MaskBits;

    /**
     * @param int $v4MaskBits
     * @param int $v6MaskBits
     */
    public function __construct(int $v4MaskBits = 24, int $v6MaskBits = 48)
    {
        $this->v4MaskBits = $v4MaskBits;
        $this->v6MaskBits = $v6MaskBits;
    }

    /**
     * Return the given subnet for an IP and the configured mask bits
     *
     * Determine if the IP is an IPv4 or IPv6 address, then pass to the correct
     * method for handling that specific type.
     *
     * @param string $ip
     * @return string
     */
    public function getSubnet(string $ip): string
    {
        if (\preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $ip)) {
            return $this->getIPv4Subnet(
                $ip,
                (int) ($this->v4MaskBits ?? 32)
            );
        }
        return $this->getIPv6Subnet(
            $ip,
            (int) ($this->v6MaskBits ?? 128)
        );
    }


    /**
     * Return the given subnet for an IPv4 address and mask bits
     *
     * @param string $ip
     * @param int $maskBits
     * @return string
     */
    public function getIPv4Subnet(string $ip, int $maskBits = 32): string
    {
        $binary = \inet_pton($ip);
        for ($i = 32; $i > $maskBits; $i -= 8) {
            $j = \intdiv($i, 8) - 1;
            $k = (int) \min(8, $i - $maskBits);
            $mask = (0xff - ((1 << $k) - 1));
            $int = \unpack('C', $binary[$j]);
            $binary[$j] = \pack('C', $int[1] & $mask);
        }
        return \inet_ntop($binary).'/'.$maskBits;
    }

    /**
     * Return the given subnet for an IPv6 address and mask bits
     *
     * @param string $ip
     * @param int $maskBits
     * @return string
     */
    public function getIPv6Subnet(string $ip, int $maskBits = 48): string
    {
        $binary = \inet_pton($ip);
        for ($i = 128; $i > $maskBits; $i -= 8) {
            $j = \intdiv($i, 8) - 1;
            $k = (int) \min(8, $i - $maskBits);
            $mask = (0xff - ((1 << $k) - 1));
            $int = \unpack('C', $binary[$j]);
            $binary[$j] = \pack('C', $int[1] & $mask);
        }
        return \inet_ntop($binary).'/'.$maskBits;
    }
}

Demo available at 3v4l.

Why Subnets instead of IP addresses?

Let's say you control an entire /24 subnet, and sent 10 bad attempts from 192.168.0.1. Your delay would increase to the max (30 seconds).

If you only blocked IP addresses, you could send another request from 192.168.0.2 and have no delay. Blocking 192.168.0.0/24 would not have the same weakness.

This problem gets greatly exacerbated when you consider that IPv6 allocations typically grant entire /48 or /64 subnets instead of a single IP, so you could theoretically burn anywhere from 2^64 to 2^80 addresses on brute force attacking before you had to suffer from rate-limiting.

So to side-step these issues, treating an entire subnet (which is configurable; by default: /24 for IPv4, /48 for IPv6) as the same source is more robust against these attacks. Since the delays are merely an inconvenience, users with legitimate credentials are never truly locked out of their account.

Scott Arciszewski
  • 33,610
  • 16
  • 89
  • 206