Preventing mass assignment com $fillable e $guarded

1. O que é Mass Assignment e por que é um Risco de Segurança?

Mass assignment é uma técnica que permite atribuir múltiplos campos de uma só vez a um modelo Eloquent, geralmente a partir de dados de formulário enviados pelo usuário. Em PHP, isso é comumente feito através dos métodos create() ou update() passando $request->all().

O perigo do mass assignment reside na possibilidade de um usuário malicioso manipular campos que não deveriam ser alterados por ele. Por exemplo, considere um formulário de cadastro que espera apenas name, email e password. Um invasor pode interceptar a requisição e adicionar campos extras como is_admin ou role_id:

// Código vulnerável
$user = User::create($request->all());

Se o modelo User não tiver proteção, o invasor poderia se tornar administrador simplesmente adicionando "is_admin": true ao payload JSON. As consequências incluem escalada de privilégios, corrupção de dados e falhas graves de autorização.

2. A Proteção Nativa do Eloquent contra Mass Assignment

Por padrão, o Eloquent do Laravel já vem com proteção ativa contra mass assignment. Se você tentar criar ou atualizar um modelo sem definir $fillable ou $guarded, uma exceção MassAssignmentException será lançada:

// Isso lançará MassAssignmentException se fillable/guarded não estiver definido
User::create(['name' => 'João', 'email' => 'joao@exemplo.com']);

É importante entender a diferença entre os métodos que são vulneráveis e os que são seguros:

  • Susceptíveis a mass assignment: create(), update(), firstOrCreate(), updateOrCreate()
  • Seguros (atribuição manual): save(), fill(), forceFill()
// Seguro - atribuição manual
$user = new User();
$user->name = 'João';
$user->email = 'joao@exemplo.com';
$user->save();

3. Usando $fillable: A Lista de Campos Permitidos

$fillable funciona como uma whitelist (lista branca) de campos que podem ser atribuídos em massa. Apenas os campos listados aqui serão aceitos pelos métodos create() e update().

Sintaxe básica:

class User extends Model
{
    protected $fillable = [
        'name',
        'email',
        'password',
    ];
}

Exemplo prático de uso:

// No controller
public function store(Request $request)
{
    // Apenas 'name', 'email' e 'password' serão atribuídos
    // Qualquer campo extra (ex: is_admin) será ignorado
    $user = User::create($request->only(['name', 'email', 'password']));

    return response()->json($user, 201);
}

Mesmo que o usuário envie is_admin no request, o Eloquent simplesmente ignorará esse campo porque ele não está em $fillable.

Boas práticas: comece com $fillable vazio e vá adicionando campos conforme a necessidade real do modelo. Isso força você a pensar explicitamente sobre quais dados podem ser atribuídos em massa.

class User extends Model
{
    // Comece vazio e adicione apenas o necessário
    protected $fillable = [];

    // Depois de validar a necessidade:
    // protected $fillable = ['name', 'email'];
}

4. Usando $guarded: A Lista de Campos Bloqueados

$guarded funciona como uma blacklist (lista negra). Todos os campos são permitidos, exceto aqueles listados aqui.

Sintaxe básica:

class User extends Model
{
    protected $guarded = [
        'is_admin',
        'password',
    ];
}

Exemplo prático:

class Log extends Model
{
    // Bloqueia apenas campos sensíveis
    protected $guarded = ['id', 'created_at', 'updated_at'];
}

Para proteção máxima, você pode usar $guarded = ['*'], que bloqueia todos os campos:

class User extends Model
{
    // Proteção máxima - nenhum campo pode ser atribuído em massa
    protected $guarded = ['*'];
}

Comparação direta:

Aspecto $fillable $guarded
Tipo Whitelist Blacklist
Segurança Mais explícito Menos explícito
Manutenção Adicionar campos conforme necessário Listar apenas campos sensíveis
Risco Menor (esqueceu de adicionar = erro) Maior (esqueceu de bloquear = vulnerabilidade)

5. Estratégias para Escolher entre $fillable e $guarded

Quando usar $fillable:
- Modelos com muitos campos sensíveis (ex: User, Admin, Payment)
- Quando você precisa de controle granular sobre cada campo
- Em projetos onde segurança é prioridade máxima

Quando usar $guarded:
- Modelos com poucos campos sensíveis (ex: Log, Audit, Notification)
- Durante prototipagem rápida quando muitos campos precisam ser acessíveis
- Modelos onde quase todos os campos são "seguros" para atribuição

Recomendação geral: prefira $fillable por ser mais explícito e seguro. O esforço extra de listar campos permitidos é um pequeno preço pela segurança adicional.

Exemplo de decisão:

// Modelo User - use $fillable (muitos campos sensíveis)
class User extends Model
{
    protected $fillable = ['name', 'email', 'password', 'avatar'];
    // is_admin, role_id, etc. ficam de fora intencionalmente
}

// Modelo Log - use $guarded (poucos campos sensíveis)
class Log extends Model
{
    protected $guarded = ['id', 'created_at'];
    // Todos os outros campos podem ser atribuídos em massa
}

6. Boas Práticas e Casos Avançados

Combinar $fillable com validação de request:

use Illuminate\Foundation\Http\FormRequest;

class StoreUserRequest extends FormRequest
{
    public function rules()
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8|confirmed',
        ];
    }
}

// No controller
public function store(StoreUserRequest $request)
{
    // Validação + fillable protegem em camadas
    $user = User::create($request->validated());
    return response()->json($user, 201);
}

Uso de DTOs para controle refinado:

class UserDTO
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
        public readonly string $password,
    ) {}

    public function toArray(): array
    {
        return [
            'name' => $this->name,
            'email' => $this->email,
            'password' => bcrypt($this->password),
        ];
    }
}

// No controller
$dto = new UserDTO(...$request->validated());
$user = User::create($dto->toArray());

Tratamento de relacionamentos:

class Post extends Model
{
    protected $fillable = ['title', 'content', 'user_id'];

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

// Criar post com usuário associado
$post = Post::create([
    'title' => 'Meu Post',
    'content' => 'Conteúdo...',
    'user_id' => auth()->id(), // Permitido porque está em $fillable
]);

Testes automatizados:

class MassAssignmentTest extends TestCase
{
    public function test_fillable_protege_campos_sensiveis()
    {
        $user = User::create([
            'name' => 'João',
            'email' => 'joao@teste.com',
            'password' => 'senha123',
            'is_admin' => true, // Este campo não está em $fillable
        ]);

        $this->assertFalse($user->is_admin);
        $this->assertEquals('João', $user->name);
    }
}

7. Erros Comuns e Como Evitá-los

Erro 1: Esquecer de definir proteção

class User extends Model
{
    // Nada definido - MassAssignmentException será lançada
}

// Solução: sempre definir $fillable ou $guarded

Erro 2: Incluir campos sensíveis no $fillable

// ERRADO - password_confirmation não deveria estar no modelo
protected $fillable = ['name', 'email', 'password', 'password_confirmation'];

// CORRETO - apenas campos que realmente vão para o banco
protected $fillable = ['name', 'email', 'password'];

Erro 3: Confundir $fillable com validação

// ERRADO - achar que $fillable substitui validação
protected $fillable = ['email']; // Apenas valida se o campo é permitido, não se é válido

// CORRETO - usar ambos em camadas
// $fillable para segurança, validação para integridade dos dados

Erro 4: Usar unguard() ou forceCreate() sem cuidado

// APENAS em cenários controlados (seeders, comandos artisan)
Model::unguard(); // Desativa proteção globalmente - use com extrema cautela

// Prefira métodos seguros
User::forceCreate([...]); // Ignora fillable/guarded - evite em produção

Referências