Коли в запиті більше полів, ніж у формі
Mass assignment означає заповнити модель масивом за один виклик: User::create($request->all()), $user->update($request->all()), fill(). Це зручно, але масив складає клієнт, а не ваша форма. У формі імʼя й пошта, а через devtools чи curl можна додати is_admin=1, role=admin або company_id=7. Якщо модель приймає кожен ключ, поле запишеться, і людина сама видасть собі права чи переїде в чужу компанію.
Eloquent вирішує, які ключі приймати, за двома списками. перелічує дозволене, решта ключів тихо відкидається. перелічує заборонене, решта приймається. За замовчуванням модель має $guarded = ['*'], і create з будь-яким ключем кидає MassAssignmentException. $guarded = [] дозволяє все. У Laravel 13 ті самі списки можна задати атрибутами класу: #[Fillable([...])], #[Guarded([...])] і #[Unguarded]. Блок-лист гірший за allow-list: нова колонка is_verified стане доступною для запиту в день міграції.
$fillable працює лише на шляху fill. forceFill і forceCreate обходять його за задумом. Model::unguard() вимикає захист для всіх моделей до reguard(), а Model::unguarded(fn) на час виклику; так працюють сидери й фабрики. update на рівні запиту, як User::whereKey($id)->update($data) чи $user->posts()->update($data), іде прямо в SQL і на $fillable не дивиться взагалі. Присвоєння по одному полю, $user->role = ..., масовим не є: його видно в коді.
Тому надійний захист стоїть на вході. повертає лише поля, для яких є правила, safe()->only([...]) звужує їх далі, а поля, які вирішує сервер (user_id, role, company_id, status), беруться з $request->user() і логіки, ніколи з запиту. Вкладений масив потребує переліку ключів: з правилом 'profile' => 'array' validated() поверне profile з усім, що прийшло. Потрібно 'array:bio,site' або правила для profile.bio і profile.site.
Помітити дірку допомагає . Model::preventSilentlyDiscardingAttributes() змушує fill кинути MassAssignmentException, коли ключ не пройшов $fillable, замість мовчки його викинути. Model::shouldBeStrict() вмикає це разом із preventLazyLoading і preventAccessingMissingAttributes. Зазвичай так: Model::shouldBeStrict(! app()->isProduction()) у сервіс-провайдері. У Livewire те саме з публічними властивостями: клієнт може змінити будь-яку з них у запиті оновлення, а #[Locked] це забороняє.
Три кроки від форми профілю до прав адміністратора
- Користувач створює POST /profile.
- POST /profile використовує Контролер ($request->all()).
- Контролер використовує User (update()).
- User надсилає подію записано, її отримує is_admin = 1.
Що проходить у модель
-
1
рядки 8–8
Allow-list: те саме, що protected $fillable = ['name', 'email'] або #[Fillable(['name', 'email'])] у Laravel 13.
-
2
рядки 14–18
Так поводиться fill після Model::preventSilentlyDiscardingAttributes(): ключ поза $fillable кидає MassAssignmentException з таким повідомленням, лише з повним іменем класу.
-
3
рядки 20–20
Без строгого режиму is_admin просто зникає, а is_admin моделі лишається false. Безпечно, але помилку у формі ніхто не помітить.
-
4
рядки 26–30
Перший рубіж стоїть до моделі: validated() бере лише поля з правилами, тож is_admin не доходить навіть до fill.
-
5
рядки 40–46
Строгий режим перетворює тихе відкидання на виняток. Вмикайте його поза production, і зайве поле впаде в тесті, а не в продакшені.
- Наведіть на пояснення, щоб підсвітити рядки.Торкніться пояснення, щоб підсвітити рядки.
$fillable захищає, крім цих місць
User::create($request->all());
Чому це дірка: all() віддає все, що надіслав клієнт, разом з полями, яких немає у формі. Від is_admin вас тоді береже лише $fillable. Передавайте $request->validated().
$post->update($request->except(['user_id', 'is_featured']));
Чому це дірка: Список заборонених полів старіє: додали колонку is_pinned, і вона вже приймається з запиту. Беріть only([...]) або safe()->only([...]).
protected $guarded = [];
Чому це дірка: Модель приймає будь-який ключ. Це нормально, лише коли fill і create отримують масиви, які склав код. Щойно туди піде масив з запиту, захисту немає.
User::whereKey($id)->update($request->all());
Чому це дірка: update на query builder або на звʼязку, як $user->posts()->update(...), іде прямо в SQL і $fillable не дивиться. Сюди потрапляють лише перевірені масиви.
'profile' => ['array'],
Чому це дірка: validated() поверне profile з усіма ключами, які прийшли, бо правил для вкладених немає. Перелічіть ключі: 'array:bio,site', або задайте правила для profile.bio і profile.site.
$user->forceFill($request->only('name', 'role'))->save();
Чому це дірка: forceFill і forceCreate обходять $fillable за задумом. Для полів, які вирішує сервер, це нормально, але не для даних з запиту.
Model::unguard();
Чому це дірка: Вимикає захист для всіх моделей до виклику reguard(). Сидери й фабрики вже працюють під Model::unguarded(). У сервіс-провайдері чи middleware це знімає $fillable з усього застосунку.
public int $companyId;
Чому це дірка: Публічну властивість компонента клієнт може змінити в запиті оновлення, як поле форми. Ідентифікатори й прапорці, які ставить сервер, позначайте #[Locked].
$serializer->deserialize($json, User::class, 'json', ['object_to_populate' => $user]);
Чому це дірка: Serializer заповнить кожну властивість, яку може записати, зокрема roles. Обмежте поля групою: 'groups' => ['user:write'], або десеріалізуйте в окремий DTO.
Де тут mass assignment
Натисніть на рядки, у яких бачите вразливість, і перевірте. Тут чотири проблеми.
Вставте у свій шаблон пул-реквесту
- create, update і fill отримують $request->validated(), safe()->only([...]) або масив, складений у коді, і ніколи all() чи except().
- Поля, які вирішує сервер (user_id, role, company_id, status, is_admin), беруться з $request->user() чи логіки, а не з запиту.
- Модель має $fillable або #[Fillable], а $guarded = [] лише там, де fill отримує масиви з коду.
- Вкладені масиви в правилах мають перелік ключів: array:bio,site або правило для кожного ключа.
- update() на запиті чи звʼязку, forceFill і forceCreate не отримують даних з запиту.
- Поза production увімкнено Model::shouldBeStrict() або preventSilentlyDiscardingAttributes().
- Публічні властивості Livewire з ідентифікаторами й прапорцями позначено #[Locked].
- Feature-тест надсилає зайве поле, як role чи is_admin, і перевіряє, що воно не змінилося.
Як цей сайт створює акаунт колеги
Власник кабінету компанії додає колегу. Валідатор пропускає лише name і email, а User::create отримує масив, складений у коді: роль, компанію й прапорець власника вирішує сервер. Дописане в запит role=admin чи company_id=7 нікуди не потрапить, бо з запиту в масив іде тільки перевірене $data.
app/Models/User.phpМодель перелічує дозволені поля атрибутом #[Fillable] з Laravel 13. role і is_company_owner у списку є, тому в create вони потрапляють лише з масиву, який склав код.src/Projects/Infrastructure/EloquentProjectRepository.phpProjectModel має $guarded = [], і це свідомо: fill отримує масив з полів доменного обʼєкта Project, а не з запиту. Власника й slug ставлять окремі присвоєння під час створення.app/Livewire/Developer/Register.phpРеєстрація розробника створює User з явним масивом і роллю developer, а прапорець social позначено #[Locked], щоб браузер не прибрав вимогу пароля.- $fillable
- Allow-list моделі: ключі, які fill, create і update приймають з масиву. Ключі поза списком відкидаються мовчки або з винятком у строгому режимі. У Laravel 13 те саме задає атрибут #[Fillable].
- $guarded
- Блок-лист моделі: ключі, які fill не приймає. ['*'] за замовчуванням блокує все, [] не блокує нічого. Атрибути #[Guarded] і #[Unguarded] роблять те саме на класі.
- validated()
- Метод FormRequest і Validator: повертає лише ті поля запиту, для яких є правила валідації. Поле без правила в результат не потрапляє, навіть якщо прийшло.
- Строгий режим Eloquent
- Model::shouldBeStrict(): заборона лінивого завантаження звʼязків, доступу до відсутніх атрибутів і тихого відкидання ключів у fill. Вмикають поза production, щоб помилки падали в тестах і локально.
Питання зі співбесід
$fillable це allow-list: модель приймає лише перелічені ключі. $guarded це блок-лист: модель приймає все, крім перелічених. Краще $fillable, бо нова колонка не стане доступною для запиту сама. За замовчуванням модель має $guarded = ['*'] і на будь-який ключ кидає MassAssignmentException.
$fillable захищає лише шлях fill і create. update на запиті чи звʼязку, forceFill, Model::unguard обходять його. До того ж role чи company_id часто законно стоять у $fillable, бо їх ставить код, і тоді all() дасть клієнту їх змінити. validated() відсікає зайве ще на вході, а поля, які вирішує сервер, беруться з $request->user().
Увесь масив profile з будь-якими ключами, які надіслав клієнт: правил для вкладених ключів немає, тож нічого не відсікається. Виправлення: 'profile' => 'array:bio,site', тоді зайвий ключ провалить валідацію, або правила для profile.bio і profile.site, тоді validated() поверне лише їх.
Model::shouldBeStrict(! app()->isProduction()): fill кидає MassAssignmentException замість тихого відкидання, і зайве поле падає в тестах. Правило рев'ю чи статичний аналіз проти all() і except() у create, update і fill. #[Fillable] або $fillable на моделях, $guarded = [] лише в інфраструктурі, де масиви складає код. Feature-тести, що надсилають role чи is_admin і перевіряють, що поле не змінилося. У Livewire #[Locked] на властивостях, які ставить сервер.