diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index a6b7f6440..6ce6b6d57 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -6,8 +6,8 @@ use App\Models\TeamInvitation; use App\Models\User; use App\Providers\RouteServiceProvider; +use Illuminate\Auth\Events\Verified; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; -use Illuminate\Foundation\Auth\EmailVerificationRequest; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Http\Request; use Illuminate\Routing\Controller as BaseController; @@ -39,9 +39,29 @@ public function verify() return view('auth.verify-email'); } - public function email_verify(EmailVerificationRequest $request) + public function email_verify(Request $request) { - $request->fulfill(); + if (! $request->hasValidSignature()) { + abort(403); + } + + $user = auth()->user(); + if (! $user) { + abort(403); + } + + if (! hash_equals((string) $request->route('id'), (string) $user->getKey())) { + abort(403); + } + + if (! hash_equals((string) $request->route('hash'), hash('sha256', $user->getEmailForVerification()))) { + abort(403); + } + + if (! $user->hasVerifiedEmail()) { + $user->markEmailAsVerified(); + event(new Verified($user)); + } return redirect(RouteServiceProvider::HOME); } diff --git a/app/Models/User.php b/app/Models/User.php index 3199d2024..237f3836f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -257,7 +257,7 @@ public function sendVerificationEmail() Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)), [ 'id' => $this->getKey(), - 'hash' => sha1($this->getEmailForVerification()), + 'hash' => hash('sha256', $this->getEmailForVerification()), ] ); $mail->view('emails.email-verification', [ diff --git a/tests/Feature/EmailVerificationHashTest.php b/tests/Feature/EmailVerificationHashTest.php new file mode 100644 index 000000000..5d42c4e44 --- /dev/null +++ b/tests/Feature/EmailVerificationHashTest.php @@ -0,0 +1,73 @@ +withoutMiddleware([DecideWhatToDoWithUser::class, CheckForcePasswordReset::class]); + Once::flush(); + if (! InstanceSettings::find(0)) { + $settings = new InstanceSettings; + $settings->id = 0; + $settings->saveQuietly(); + } +}); + +describe('email verification hash', function () { + test('sha256 hash is accepted and marks the user verified', function () { + $user = User::factory()->create([ + 'email' => 'verify-me@example.com', + 'email_verified_at' => null, + ]); + + $url = URL::temporarySignedRoute('verify.verify', now()->addHour(), [ + 'id' => $user->getKey(), + 'hash' => hash('sha256', $user->getEmailForVerification()), + ]); + + $this->actingAs($user)->get($url)->assertRedirect(); + + $user->refresh(); + expect($user->email_verified_at)->not->toBeNull(); + }); + + test('legacy sha1 hash is rejected', function () { + $user = User::factory()->create([ + 'email' => 'legacy-sha1@example.com', + 'email_verified_at' => null, + ]); + + $url = URL::temporarySignedRoute('verify.verify', now()->addHour(), [ + 'id' => $user->getKey(), + 'hash' => sha1($user->getEmailForVerification()), + ]); + + $this->actingAs($user)->get($url)->assertStatus(403); + + $user->refresh(); + expect($user->email_verified_at)->toBeNull(); + }); + + test('tampered signature is rejected', function () { + $user = User::factory()->create([ + 'email' => 'tampered@example.com', + 'email_verified_at' => null, + ]); + + $url = URL::temporarySignedRoute('verify.verify', now()->addHour(), [ + 'id' => $user->getKey(), + 'hash' => hash('sha256', $user->getEmailForVerification()), + ]); + + $tampered = $url.'x'; + + $this->actingAs($user)->get($tampered)->assertStatus(403); + }); +});