From ba3cc6fe91986b54ec60b957109bf58ec2e00438 Mon Sep 17 00:00:00 2001 From: hans362 Date: Mon, 26 Jan 2026 10:07:38 +0800 Subject: [PATCH 1/4] fix: generate temporary email verification link with email hash included --- app/Http/Controllers/AuthController.php | 6 +++--- app/Http/Controllers/UserController.php | 2 +- app/Listeners/SendEmailVerification.php | 2 +- routes/web.php | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 3b10ce2c..1f224cf8 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -344,20 +344,20 @@ class AuthController extends Controller return redirect('/user'); } - public function verify(Request $request) + public function verify(Request $request, User $user) { if (!option('require_verification')) { throw new PrettyPageException(trans('user.verification.disabled'), 1); } - abort_unless($request->hasValidSignature(false), 403, trans('auth.verify.invalid')); + abort_unless($request->hasValidSignature(false) && hash_equals((string)$request->route('hash'), sha1($user->email)), 403, trans('auth.verify.invalid')); return view('auth.verify'); } public function handleVerify(Request $request, User $user) { - abort_unless($request->hasValidSignature(false), 403, trans('auth.verify.invalid')); + abort_unless($request->hasValidSignature(false) && hash_equals((string)$request->route('hash'), sha1($user->email)), 403, trans('auth.verify.invalid')); ['email' => $email] = $request->validate(['email' => 'required|email']); diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index c464f992..b5c63b15 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -157,7 +157,7 @@ class UserController extends Controller return json(trans('user.verification.verified'), 1); } - $url = URL::signedRoute('auth.verify', ['user' => $user], null, false); + $url = URL::temporarySignedRoute('auth.verify', Carbon::now()->addHour(), ['user' => $user, 'hash' => sha1($user->email)], false); try { Mail::to($user->email)->send(new EmailVerification(url($url))); diff --git a/app/Listeners/SendEmailVerification.php b/app/Listeners/SendEmailVerification.php index 097fd169..35ebadfb 100644 --- a/app/Listeners/SendEmailVerification.php +++ b/app/Listeners/SendEmailVerification.php @@ -12,7 +12,7 @@ class SendEmailVerification public function handle(User $user) { if (option('require_verification')) { - $url = URL::signedRoute('auth.verify', ['user' => $user->uid], null, false); + $url = URL::temporarySignedRoute('auth.verify', Carbon::now()->addHour(), ['user' => $user, 'hash' => sha1($user->email)], false); try { Mail::to($user->email)->send(new EmailVerification(url($url))); diff --git a/routes/web.php b/routes/web.php index 06cb4ba9..0f5cdf94 100644 --- a/routes/web.php +++ b/routes/web.php @@ -41,8 +41,8 @@ Route::prefix('auth')->name('auth.')->group(function () { Route::post('bind', 'AuthController@fillEmail')->name('verify'); }); - Route::get('verify/{user}', 'AuthController@verify')->name('verify'); - Route::post('verify/{user}', 'AuthController@handleVerify')->name('handle.verify'); + Route::get('verify/{user}/{hash}', 'AuthController@verify')->name('verify'); + Route::post('verify/{user}/{hash}', 'AuthController@handleVerify')->name('handle.verify'); }); Route::prefix('user') From 2b1ee0344e4b8725de50496fba183f5f8a5ad2e8 Mon Sep 17 00:00:00 2001 From: hans362 Date: Mon, 26 Jan 2026 17:37:56 +0800 Subject: [PATCH 2/4] fix: add missing Carbon import in SendEmailVerification listener --- app/Listeners/SendEmailVerification.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Listeners/SendEmailVerification.php b/app/Listeners/SendEmailVerification.php index 35ebadfb..563b193d 100644 --- a/app/Listeners/SendEmailVerification.php +++ b/app/Listeners/SendEmailVerification.php @@ -4,6 +4,7 @@ namespace App\Listeners; use App\Mail\EmailVerification; use App\Models\User; +use Carbon\Carbon; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\URL; From 5b482f24685398e78084bebfbb83d1fff17a2b5b Mon Sep 17 00:00:00 2001 From: hans362 Date: Thu, 29 Jan 2026 09:55:35 +0800 Subject: [PATCH 3/4] fix: update tests for email verification --- tests/HttpTest/ControllersTest/AuthControllerTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/HttpTest/ControllersTest/AuthControllerTest.php b/tests/HttpTest/ControllersTest/AuthControllerTest.php index 8331557f..0c3c1f05 100644 --- a/tests/HttpTest/ControllersTest/AuthControllerTest.php +++ b/tests/HttpTest/ControllersTest/AuthControllerTest.php @@ -724,7 +724,7 @@ class AuthControllerTest extends TestCase public function testVerify() { - $url = URL::signedRoute('auth.verify', ['user' => 1], null, false); + $url = URL::temporarySignedRoute('auth.verify', Carbon::now()->addHour(), ['user' => 1, 'hash' => sha1('a@b.c')], false); // should be forbidden if account verification is disabled option(['require_verification' => false]); @@ -732,17 +732,17 @@ class AuthControllerTest extends TestCase option(['require_verification' => true]); // invalid link - $this->get(route('auth.verify', ['user' => 1]))->assertForbidden(); + $this->get(route('auth.verify', ['user' => 1, 'hash' => sha1('a@b.c')]))->assertForbidden(); $user = User::factory()->create(['verified' => false]); - $url = URL::signedRoute('auth.verify', ['user' => $user], null, false); + $url = URL::temporarySignedRoute('auth.verify', Carbon::now()->addHour(), ['user' => $user, 'hash' => sha1($user->email)], false); $this->get($url)->assertViewIs('auth.verify'); } public function testHandleVerify() { $user = User::factory()->create(['verified' => false]); - $url = URL::signedRoute('auth.verify', ['user' => $user], null, false); + $url = URL::temporarySignedRoute('auth.verify', Carbon::now()->addHour(), ['user' => $user, 'hash' => sha1($user->email)], false); // empty email $this->post($url, [], ['Referer' => $url])->assertRedirect($url); From 3439917b33d2e72845a227f76695d7b306c2fda0 Mon Sep 17 00:00:00 2001 From: hans362 Date: Wed, 11 Mar 2026 16:47:49 +0800 Subject: [PATCH 4/4] feat: replace sha1 with sha256 --- app/Http/Controllers/AuthController.php | 4 ++-- app/Http/Controllers/UserController.php | 2 +- app/Listeners/SendEmailVerification.php | 2 +- tests/HttpTest/ControllersTest/AuthControllerTest.php | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 1f224cf8..9e9478ba 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -350,14 +350,14 @@ class AuthController extends Controller throw new PrettyPageException(trans('user.verification.disabled'), 1); } - abort_unless($request->hasValidSignature(false) && hash_equals((string)$request->route('hash'), sha1($user->email)), 403, trans('auth.verify.invalid')); + abort_unless($request->hasValidSignature(false) && hash_equals((string)$request->route('hash'), hash('sha256', $user->email)), 403, trans('auth.verify.invalid')); return view('auth.verify'); } public function handleVerify(Request $request, User $user) { - abort_unless($request->hasValidSignature(false) && hash_equals((string)$request->route('hash'), sha1($user->email)), 403, trans('auth.verify.invalid')); + abort_unless($request->hasValidSignature(false) && hash_equals((string)$request->route('hash'), hash('sha256', $user->email)), 403, trans('auth.verify.invalid')); ['email' => $email] = $request->validate(['email' => 'required|email']); diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index b5c63b15..de36021d 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -157,7 +157,7 @@ class UserController extends Controller return json(trans('user.verification.verified'), 1); } - $url = URL::temporarySignedRoute('auth.verify', Carbon::now()->addHour(), ['user' => $user, 'hash' => sha1($user->email)], false); + $url = URL::temporarySignedRoute('auth.verify', Carbon::now()->addHour(), ['user' => $user, 'hash' => hash('sha256', $user->email)], false); try { Mail::to($user->email)->send(new EmailVerification(url($url))); diff --git a/app/Listeners/SendEmailVerification.php b/app/Listeners/SendEmailVerification.php index 563b193d..d1236ab1 100644 --- a/app/Listeners/SendEmailVerification.php +++ b/app/Listeners/SendEmailVerification.php @@ -13,7 +13,7 @@ class SendEmailVerification public function handle(User $user) { if (option('require_verification')) { - $url = URL::temporarySignedRoute('auth.verify', Carbon::now()->addHour(), ['user' => $user, 'hash' => sha1($user->email)], false); + $url = URL::temporarySignedRoute('auth.verify', Carbon::now()->addHour(), ['user' => $user, 'hash' => hash('sha256', $user->email)], false); try { Mail::to($user->email)->send(new EmailVerification(url($url))); diff --git a/tests/HttpTest/ControllersTest/AuthControllerTest.php b/tests/HttpTest/ControllersTest/AuthControllerTest.php index 0c3c1f05..860dd75c 100644 --- a/tests/HttpTest/ControllersTest/AuthControllerTest.php +++ b/tests/HttpTest/ControllersTest/AuthControllerTest.php @@ -724,7 +724,7 @@ class AuthControllerTest extends TestCase public function testVerify() { - $url = URL::temporarySignedRoute('auth.verify', Carbon::now()->addHour(), ['user' => 1, 'hash' => sha1('a@b.c')], false); + $url = URL::temporarySignedRoute('auth.verify', Carbon::now()->addHour(), ['user' => 1, 'hash' => hash('sha256', 'a@b.c')], false); // should be forbidden if account verification is disabled option(['require_verification' => false]); @@ -732,17 +732,17 @@ class AuthControllerTest extends TestCase option(['require_verification' => true]); // invalid link - $this->get(route('auth.verify', ['user' => 1, 'hash' => sha1('a@b.c')]))->assertForbidden(); + $this->get(route('auth.verify', ['user' => 1, 'hash' => hash('sha256', 'a@b.c')]))->assertForbidden(); $user = User::factory()->create(['verified' => false]); - $url = URL::temporarySignedRoute('auth.verify', Carbon::now()->addHour(), ['user' => $user, 'hash' => sha1($user->email)], false); + $url = URL::temporarySignedRoute('auth.verify', Carbon::now()->addHour(), ['user' => $user, 'hash' => hash('sha256', $user->email)], false); $this->get($url)->assertViewIs('auth.verify'); } public function testHandleVerify() { $user = User::factory()->create(['verified' => false]); - $url = URL::temporarySignedRoute('auth.verify', Carbon::now()->addHour(), ['user' => $user, 'hash' => sha1($user->email)], false); + $url = URL::temporarySignedRoute('auth.verify', Carbon::now()->addHour(), ['user' => $user, 'hash' => hash('sha256', $user->email)], false); // empty email $this->post($url, [], ['Referer' => $url])->assertRedirect($url);