feat(auth): block reserved identifiers at sign-up
Add App\Rules\ReservedIdentifier for name + email local-part so users cannot impersonate staff (admin, superadmin, support, root, takeone, …). Separators are collapsed so "super_admin"/"super-admin"/"superadmin" all match. Rule wired into RegisteredUserController@store. email_verified_at is already absent from User::$fillable so only the signed verification route can flip it — audited, no code change needed. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
56b6a83b34
commit
a1493462f7
2 files changed
+59
-2
No files matched your search
@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Notifications\NewUserRegistered;
|
use App\Notifications\NewUserRegistered;
|
||||||
use App\Rules\NotDisposableEmail;
|
use App\Rules\NotDisposableEmail;
|
||||||
|
use App\Rules\ReservedIdentifier;
|
||||||
use Illuminate\Auth\Events\Registered;
|
use Illuminate\Auth\Events\Registered;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
@@ -27,8 +28,8 @@ class RegisteredUserController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255', new ReservedIdentifier('name')],
|
||||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users', new NotDisposableEmail],
|
'email' => ['required', 'string', 'email', 'max:255', 'unique:users', new NotDisposableEmail, new ReservedIdentifier('email')],
|
||||||
'password' => ['required', 'confirmed', Password::defaults()],
|
'password' => ['required', 'confirmed', Password::defaults()],
|
||||||
'birthday' => ['required', 'date', 'before:today'],
|
'birthday' => ['required', 'date', 'before:today'],
|
||||||
'gender' => ['required', 'in:male,female'],
|
'gender' => ['required', 'in:male,female'],
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Rules;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blocks impersonation of staff/system accounts.
|
||||||
|
*
|
||||||
|
* Applied to both `name` and the email local-part at registration so a user
|
||||||
|
* cannot sign up as "admin", "superadmin", "takeone", etc. — words that would
|
||||||
|
* let them pass themselves off as the platform or its operators to other
|
||||||
|
* users (comments, DMs, channel pages).
|
||||||
|
*/
|
||||||
|
class ReservedIdentifier implements ValidationRule
|
||||||
|
{
|
||||||
|
private const RESERVED = [
|
||||||
|
'admin', 'administrator', 'superadmin', 'super-admin', 'super_admin',
|
||||||
|
'root', 'sysadmin', 'system', 'owner', 'webmaster', 'postmaster',
|
||||||
|
'moderator', 'mod', 'staff', 'official',
|
||||||
|
'support', 'help', 'helpdesk', 'contact', 'info', 'noreply', 'no-reply',
|
||||||
|
'security', 'abuse', 'billing',
|
||||||
|
'takeone', 'take-one', 'take_one', 'takeonebh', 'takeone-bh',
|
||||||
|
'play', 'playbh', 'playtakeone',
|
||||||
|
'anonymous', 'guest', 'null', 'undefined',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(private readonly string $kind = 'name')
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||||
|
{
|
||||||
|
if (! is_string($value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidate = $this->kind === 'email'
|
||||||
|
? strtolower(trim(strstr($value, '@', true) ?: $value))
|
||||||
|
: strtolower(trim($value));
|
||||||
|
|
||||||
|
// Collapse separators so "super_admin", "super-admin" and "superadmin" all match.
|
||||||
|
$normalized = preg_replace('/[\s._\-]+/', '', $candidate) ?? $candidate;
|
||||||
|
|
||||||
|
foreach (self::RESERVED as $word) {
|
||||||
|
$target = preg_replace('/[\s._\-]+/', '', $word) ?? $word;
|
||||||
|
if ($normalized === $target) {
|
||||||
|
$fail($this->kind === 'email'
|
||||||
|
? 'That email address is reserved. Please use a different one.'
|
||||||
|
: 'That name is reserved. Please choose a different one.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user