diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 4556023f3..f50615753 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -13,10 +13,8 @@ use Illuminate\Foundation\Auth\RegistersUsers; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; -use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; -use Illuminate\Support\Str; use Purify; class RegisterController extends Controller @@ -51,13 +49,6 @@ class RegisterController extends Controller $this->middleware('guest'); } - public function getRegisterToken() - { - return Cache::remember('pf:register:rt', 900, function () { - return Str::random(40); - }); - } - /** * Get a validator for an incoming registration request. * @@ -93,18 +84,8 @@ class RegisterController extends Controller }, ]; - $rt = [ - 'required', - function ($attribute, $value, $fail) { - if ($value !== $this->getRegisterToken()) { - return $fail('Something went wrong'); - } - }, - ]; - $rules = [ 'agecheck' => 'required|accepted', - 'rt' => $rt, 'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'), 'username' => $usernameRules, 'email' => $emailRules, diff --git a/composer.json b/composer.json index 940af36e6..e2d802f69 100644 --- a/composer.json +++ b/composer.json @@ -43,6 +43,7 @@ "pusher/pusher-php-server": "^7.2", "resend/resend-php": "^0.13.0", "spatie/laravel-backup": "^9.2.9", + "spatie/laravel-honeypot": "^4.7", "spatie/laravel-image-optimizer": "^1.8.2", "stevebauman/purify": "^6.2.0", "symfony/http-client": "^7.3", diff --git a/composer.lock b/composer.lock index 99cdfb267..fbeb15cd7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d438c452baff3ac7487931d7bbbf2d68", + "content-hash": "418cf8d4002b49158c5ea0b6bd4dcf4e", "packages": [ { "name": "aws/aws-crt-php", @@ -6813,6 +6813,82 @@ ], "time": "2026-02-15T19:05:20+00:00" }, + { + "name": "spatie/laravel-honeypot", + "version": "4.7.2", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-honeypot.git", + "reference": "b99f668c5b673ddc8abbc58743584339cc8db3a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-honeypot/zipball/b99f668c5b673ddc8abbc58743584339cc8db3a9", + "reference": "b99f668c5b673ddc8abbc58743584339cc8db3a9", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/encryption": "^11.0|^12.0|^13.0", + "illuminate/http": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "illuminate/validation": "^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.0|^3.0", + "php": "^8.2", + "spatie/laravel-package-tools": "^1.9", + "symfony/http-foundation": "^7.0|^8.0" + }, + "require-dev": { + "livewire/livewire": "^3.0|^4.0", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "pestphp/pest": "^2.0|^3.0|^4.0", + "pestphp/pest-plugin-livewire": "^1.0|^2.1|^3.0|^4.0", + "spatie/pest-plugin-snapshots": "^1.1|^2.1", + "spatie/phpunit-snapshot-assertions": "^4.2|^5.1", + "spatie/test-time": "^1.2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Honeypot\\HoneypotServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\Honeypot\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Preventing spam submitted through forms", + "homepage": "https://github.com/spatie/laravel-honeypot", + "keywords": [ + "laravel-honeypot", + "spatie" + ], + "support": { + "source": "https://github.com/spatie/laravel-honeypot/tree/4.7.2" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + } + ], + "time": "2026-06-25T15:09:40+00:00" + }, { "name": "spatie/laravel-image-optimizer", "version": "1.8.3", diff --git a/config/honeypot.php b/config/honeypot.php new file mode 100644 index 000000000..52c2c9f2e --- /dev/null +++ b/config/honeypot.php @@ -0,0 +1,72 @@ + env('HONEYPOT_ENABLED', true), + + /* + * Here you can specify name of the honeypot field. Any requests that submit a non-empty + * value for this name will be discarded. Make sure this name does not + * collide with a form field that is actually used. + */ + 'name_field_name' => env('HONEYPOT_NAME', 'my_name'), + + /* + * When this is activated there will be a random string added + * to the name_field_name. This improves the + * protection against bots. + */ + 'randomize_name_field_name' => env('HONEYPOT_RANDOMIZE', true), + + /* + * When this is activated, requests will be checked if + * form is submitted faster than this amount of seconds + */ + 'valid_from_timestamp' => env('HONEYPOT_VALID_FROM_TIMESTAMP', true), + + /* + * This field contains the name of a form field that will be used to verify + * if the form wasn't submitted too quickly. Make sure this name does not + * collide with a form field that is actually used. + */ + 'valid_from_field_name' => env('HONEYPOT_VALID_FROM', 'valid_from'), + + /* + * If the form is submitted faster than this amount of seconds + * the form submission will be considered invalid. + */ + 'amount_of_seconds' => (int) env('HONEYPOT_SECONDS', 1), + + /* + * This class is responsible for sending a response to requests that + * are detected as being spammy. By default a blank page is shown. + * + * A valid responder is any class that implements + * `Spatie\Honeypot\SpamResponder\SpamResponder` + */ + 'respond_to_spam_with' => BlankPageResponder::class, + + /* + * When activated, requests will be checked if honeypot fields are missing, + * if so the request will be stamped as spam. Be careful! When using the + * global middleware be sure to add honeypot fields to each form. + */ + 'honeypot_fields_required_for_all_forms' => false, + + /* + * This class is responsible for applying all spam protection + * rules for a request. In most cases, you shouldn't change + * this value. + */ + 'spam_protection' => SpamProtection::class, + + /* + * need to add @cspNonce https://github.com/spatie/laravel-csp in style tag hidden items + */ + 'with_csp' => env('HONEYPOT_WITH_CSP', false), +]; diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index 83c336f8b..734e72381 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -10,7 +10,7 @@
@csrf - + @honeypot
diff --git a/resources/views/settings/parental-controls/invite-register-form.blade.php b/resources/views/settings/parental-controls/invite-register-form.blade.php index a21808efa..eb0d89a52 100644 --- a/resources/views/settings/parental-controls/invite-register-form.blade.php +++ b/resources/views/settings/parental-controls/invite-register-form.blade.php @@ -32,8 +32,7 @@
@csrf - - + @honeypot
diff --git a/routes/web.php b/routes/web.php index 3d7131ee0..811bb97e1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -4,6 +4,7 @@ use App\Http\Controllers\AccountController; use App\Http\Controllers\AccountInterstitialController; use App\Http\Controllers\AdminInviteController; use App\Http\Controllers\AppRegisterController; +use App\Http\Controllers\Auth\RegisterController; use App\Http\Controllers\AuthorizeInteractionController; use App\Http\Controllers\AvatarController; use App\Http\Controllers\BookmarkController; @@ -53,6 +54,7 @@ use Laravel\Passport\Http\Controllers\AuthorizedAccessTokenController; use Laravel\Passport\Http\Controllers\ClientController; use Laravel\Passport\Http\Controllers\DenyAuthorizationController; use Laravel\Passport\Http\Controllers\TransientTokenController; +use Spatie\Honeypot\ProtectAgainstSpam; Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofactor', 'localization'])->group(function () { Route::get('/', [SiteController::class, 'home'])->name('timeline.personal'); @@ -63,6 +65,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Auth::routes(); + Route::post('register', [RegisterController::class, 'register'])->middleware(ProtectAgainstSpam::class); + Route::get('auth/oidc/start', [RemoteOidcController::class, 'start']); Route::get('auth/oidc/callback', [RemoteOidcController::class, 'handleCallback']); @@ -86,7 +90,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::post('auth/raw/mastodon/s/finish-up', [RemoteAuthController::class, 'finishUp']); Route::post('auth/raw/mastodon/s/login', [RemoteAuthController::class, 'handleLogin']); Route::get('auth/pci/{id}/{code}', [ParentalControlsController::class, 'inviteRegister']); - Route::post('auth/pci/{id}/{code}', [ParentalControlsController::class, 'inviteRegisterStore']); + Route::post('auth/pci/{id}/{code}', [ParentalControlsController::class, 'inviteRegisterStore'])->middleware(ProtectAgainstSpam::class); Route::get('auth/sign_up', [SiteController::class, 'curatedOnboarding'])->name('auth.curated-onboarding'); Route::post('auth/sign_up', [CuratedRegisterController::class, 'proceed']); diff --git a/tests/Feature/Auth/RegisterTest.php b/tests/Feature/Auth/RegisterTest.php index d87c9b14f..8cfc6bd9d 100644 --- a/tests/Feature/Auth/RegisterTest.php +++ b/tests/Feature/Auth/RegisterTest.php @@ -23,9 +23,9 @@ it('creates a user with valid registration data', function () { config(['pixelfed.max_users' => 1000]); config(['instance.enable_cc' => false]); - // Visit the register page first to seed the RT token in cache - $this->get('/register')->assertOk(); - $rt = cache()->get('pf:register:rt'); + // Disable the honeypot spam protection so the test submission isn't + // flagged for being submitted faster than the minimum timestamp threshold. + config(['honeypot.enabled' => false]); $response = $this->post('/register', [ 'name' => 'Test User', @@ -35,7 +35,6 @@ it('creates a user with valid registration data', function () { 'password_confirmation' => 'SecurePass123!', 'agree' => 'on', 'agecheck' => 'on', - 'rt' => $rt, ]); $response->assertRedirect();