Laravel: Tackling down the breadth-first attack

No need to provision a full firewall for just something so easy to fix

The default code that throttles logins in Laravel is very basic: it will throttle the combination of the device IP and the user ID (the email by default) if the login fails. That’s it.

Okay, that will work if someone tries “guess” the user password. But the problem with that implementation is that you can try with other users, since the throttling will start fresh new when you change the email. Rinse and repeat:

Over 800 login attempts were made without triggering the application’s rate-limiting mechanism.

This is a big problem since there are a lot of bots that take email list with vulnerable passwords, navigate around the internet for applications, and check if the user has an account with the same password so it can be hijacked, or even attempt an elaborate phishing plan.

There are a couple of solutions, though.

Solution A: Throttle the IP

Since the problem with the throttling is the combination of the device IP plus the User ID, we can just eliminate the User ID from the equation. This will throttle the IP altogether.

Because some IP may be behind a NAT, this may throttle the whole network behind it because of some rogue device trying to force its way in, but personally that is a problem that cannot be tackled by you unless you use HTTP_X_FORWARDED_FOR, and even then, is an unreliable header since it can be changed, while the real IP can’t (unless you’re behind a load balancer)

Anyway, assuming your app will be fine, we can just override the throttle key, which is conveniently returned by the throttleKey() method, in your LoginController:

/**
* Get the throttle key for the given request.
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function throttleKey(Request $request)
{
return Str::lower(get_class($this). '|' . $request->ip());
}

We will use the get_class() method to avoid throttling the whole application, but just the controller itself. You can also use the Request path using $request->path(). At your discretion.

Now, consider this as a not sane option. Okay, it may work for IPv4 addresses, but for IPv6, you will have to have patience since there are more than 4.3 billion and someone could just renew its IP to anything else.

Solution B: Put a f*cking reCAPTCHA

I’m gonna get yelled, but there is no other reliable solution apart from throttling the IP and stop a bot in their tracks. reCAPTCHA is a good way to protect your forms against bots, which in a default installation are handled by the Login, Registration, Verification Resend and Password Forgot controllers.

While some may just put a reCAPTCHA checkbox in their forms, for me the most simple solution is to integrate Invisible reCAPTCHA (v2) and automatically bind it to the submit button of your page:

The invisible reCAPTCHA badge does not require the user to click on a checkbox, instead it is invoked directly when the user clicks on an existing button on your site or can be invoked via a JavaScript API call. (…) By default only the most suspicious traffic will be prompted to solve a captcha.

Behind the scenes, you can just use the official reCAPTCHA PHP client to verify the challenge using a middleware:

<?php

namespace App\Http\Controllers\Middleware;

use Closure;
use ReCaptcha\ReCaptcha;
use Illuminate\Validation\ValidationException;

class CheckReCaptcha
{
/**
* Handle an incoming request.
*
*
@param \Illuminate\Http\Request $request
*
@param \Closure $next
*
@return mixed
*
@throws \Illuminate\Validation\ValidationException
*/
public function handle($request, Closure $next)
{
$recaptcha = new ReCaptcha(config('recaptcha.invisible.secret'));

$response = $recaptcha->verify($request->input('g-recaptcha-response'), $request->ip());

if (!$response->isSuccess()) {
throw ValidationException::withMessages([
'recaptcha' => 'Please solve the reCAPTCHA again'
])->redirectTo(back()->getTargetUrl());
}

return $next($request);
}
}

The Invisible reCAPTCHA will appear in the form only if the interaction is deemed suspicious, so you won’t need to do anything more than including the middleware into your routes:

Route::post('login', 'LoginController@login')
->middleware('App\Http\Controllers\Middleware\CheckReCaptcha')

And walá. No more breadth-first attack.

Get smarter at building your thing. Join The Startup’s +792K followers.

By The Startup

Get smarter at building your thing. Subscribe to receive The Startup's top 10 most read stories — delivered straight into your inbox, once a week. Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Italo Baeza Cabrera

Written by

Graphic Designer graduate. Full Stack Web Developer. Retired Tech & Gaming Editor.

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +792K followers.

Italo Baeza Cabrera

Written by

Graphic Designer graduate. Full Stack Web Developer. Retired Tech & Gaming Editor.

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +792K followers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store