fix(security): harden password reset email code validation

This commit is contained in:
xboard 2026-05-22 21:50:29 +08:00
parent f2ac5db4bd
commit 1eb085d4c0
3 changed files with 88 additions and 3 deletions

View File

@ -16,7 +16,7 @@ class AuthForget extends FormRequest
return [
'email' => 'required|email:strict',
'password' => 'required|min:8',
'email_code' => 'required'
'email_code' => 'required|digits:6'
];
}
@ -27,7 +27,8 @@ class AuthForget extends FormRequest
'email.email' => __('Email format is incorrect'),
'password.required' => __('Password can not be empty'),
'password.min' => __('Password must be greater than 8 digits'),
'email_code.required' => __('Email verification code cannot be empty')
'email_code.required' => __('Email verification code cannot be empty'),
'email_code.digits' => __('Incorrect email verification code')
];
}
}

View File

@ -93,7 +93,8 @@ class LoginService
}
// 验证邮箱验证码
if ((string) Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $email)) !== (string) $emailCode) {
$cachedEmailCode = Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $email));
if ($cachedEmailCode === null || !hash_equals((string) $cachedEmailCode, $emailCode)) {
Cache::put($forgetRequestLimitKey, $forgetRequestLimit ? $forgetRequestLimit + 1 : 1, 300);
return [false, [400, __('Incorrect email verification code')]];
}

View File

@ -0,0 +1,83 @@
<?php
namespace Tests\Unit\Services\Auth;
use App\Models\User;
use App\Services\Auth\LoginService;
use App\Utils\CacheKey;
use App\Utils\Helper;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
use Tests\TestCase;
class LoginServiceTest extends TestCase
{
use RefreshDatabase;
private LoginService $service;
protected function setUp(): void
{
parent::setUp();
Cache::flush();
$this->service = app(LoginService::class);
}
public function test_reset_password_rejects_missing_cached_email_code(): void
{
$user = $this->createUser('victim@example.com', 'old-password');
[$success, $result] = $this->service->resetPassword($user->email, '', 'new-password');
$this->assertFalse($success);
$this->assertSame(400, $result[0]);
$user->refresh();
$this->assertTrue(password_verify('old-password', $user->password));
}
public function test_reset_password_accepts_matching_cached_email_code(): void
{
$user = $this->createUser('user@example.com', 'old-password');
Cache::put(CacheKey::get('EMAIL_VERIFY_CODE', $user->email), 123456, 300);
[$success, $result] = $this->service->resetPassword($user->email, '123456', 'new-password');
$this->assertTrue($success);
$this->assertTrue($result);
$user->refresh();
$this->assertTrue(password_verify('new-password', $user->password));
$this->assertNull(Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $user->email)));
}
public function test_forget_password_validation_rejects_boolean_email_code(): void
{
$validator = Validator::make([
'email' => 'victim@example.com',
'password' => 'new-password',
'email_code' => false,
], [
'email' => 'required|email:strict',
'password' => 'required|min:8',
'email_code' => 'required|digits:6',
]);
$this->assertTrue($validator->fails());
$this->assertArrayHasKey('email_code', $validator->errors()->toArray());
}
private function createUser(string $email, string $password): User
{
return User::query()->create([
'email' => $email,
'password' => password_hash($password, PASSWORD_DEFAULT),
'uuid' => Helper::guid(true),
'token' => Helper::guid(true),
'created_at' => time(),
'updated_at' => time(),
]);
}
}