Merge pull request #7146 from shleeable/fix/2fa-checkpoint-throttle

Rate limit and audit-log 2FA checkpoint verification
pull/7147/head
Shlee 2 weeks ago committed by GitHub
commit 46fb9613e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -6,6 +6,7 @@ use App\Jobs\FollowPipeline\FollowAcceptPipeline;
use App\Jobs\FollowPipeline\FollowPipeline;
use App\Jobs\FollowPipeline\FollowRejectPipeline;
use App\Mail\ConfirmEmail;
use App\Models\AccountLog;
use App\Models\EmailVerification;
use App\Models\Follower;
use App\Models\FollowRequest;
@ -521,6 +522,19 @@ class AccountController extends Controller
return redirect('/');
}
// Audit failed 2FA verification so brute-force attempts at the MFA
// layer are visible (the route throttle bounds the rate per user).
$log = new AccountLog;
$log->user_id = $user->id;
$log->item_id = $user->id;
$log->item_type = User::class;
$log->action = 'auth.2fa.failed';
$log->message = '2FA verification failed';
$log->link = null;
$log->ip_address = $request->ip();
$log->user_agent = $request->userAgent();
$log->save();
if ($request->session()->has('2fa.attempts')) {
$count = (int) $request->session()->get('2fa.attempts');
if ($count == 3) {

@ -196,7 +196,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('auth/sudo', [AccountController::class, 'confirmPassword'])->name('password.confirm');
Route::post('auth/sudo', [AccountController::class, 'confirmPasswordStore']);
Route::get('auth/checkpoint', [AccountController::class, 'twoFactorCheckpoint']);
Route::post('auth/checkpoint', [AccountController::class, 'twoFactorVerify']);
Route::post('auth/checkpoint', [AccountController::class, 'twoFactorVerify'])
->middleware('throttle:5,15');
Route::get('results', [SearchController::class, 'results']);
Route::post('visibility', [StatusController::class, 'toggleVisibility']);

@ -0,0 +1,49 @@
<?php
use App\Models\AccountLog;
use App\Models\User;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Route;
use PragmaRX\Google2FA\Google2FA;
uses(LazilyRefreshDatabase::class);
/*
|--------------------------------------------------------------------------
| 2FA checkpoint rate limiting + audit
|--------------------------------------------------------------------------
|
| The 2FA verify endpoint must be server-side rate limited (so re-login cannot
| reset an unlimited guess budget) and failed attempts must be audit-logged.
|
*/
it('applies throttle middleware to the 2FA verify route', function () {
$route = collect(Route::getRoutes())->first(function ($r) {
return $r->uri() === 'i/auth/checkpoint' && in_array('POST', $r->methods());
});
expect($route)->not->toBeNull();
$hasThrottle = collect($route->gatherMiddleware())
->contains(fn ($m) => is_string($m) && str_starts_with($m, 'throttle'));
expect($hasThrottle)->toBeTrue();
});
it('audit-logs a failed 2FA verification', function () {
$google2fa = new Google2FA;
$secret = $google2fa->generateSecretKey();
$user = User::factory()->create(['2fa_secret' => $secret, '2fa_enabled' => true]);
$user->refresh();
$this->actingAs($user)
->post('/i/auth/checkpoint', ['code' => '000000']);
expect(
AccountLog::where('user_id', $user->id)
->where('action', 'auth.2fa.failed')
->exists()
)->toBeTrue();
});
Loading…
Cancel
Save