1

Quando a validação é inútil

Vou falar sobre uma vulnerabilidade que já peguei em um projeto PHP com Laravel e apontar um problema de design do Laravel que contribuiu com essa vulnerabilidade.

Validação de inputs é algo que até mesmo os estagiários aprendem. É tão trivial e rotineiro que os desenvolvedores nem pensam muito nisso, só vão em fazem a validação. Talvez justamente por confiarem demais na trivialidade da tarefa, e pelo framework já entregar recursos prontos para fazer validação, que acabam negligenciando a tarefa.

Já peguei várias vulnerabilidades relacionadas a validação de inputs com Laravel, mas essa especificamente em parte é culpa do próprio Laravel também.

Em um endpoint PUT de uma REST API, no FormRequest do endpoint, era feito a validação do campo $this->id afim de verificar se o usuário autenticado tinha permissão para alterar aquele recurso. Se não tivesse, ele recebia um 403 e tudo certo. Algo parecido com isto:

use App\Models\Post;
use Illuminate\Foundation\Http\FormRequest;

class UpdatePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        $post = Post::find($this->id);

        if (!$post) {
            return false;
        }

        return $this->user()->id === $post->user_id;
    }

    public function rules(): array
    {
        return [
            'title' => ['required', 'string'],
        ];
    }
}

No controller do endpoint, o objeto atualizado no banco de dados (usando sua model) era obtido usando o segundo argumento do método, $postId, enquanto no UpdatePostRequest era validado o campo $this->id. O controller era mais ou menos assim:

use App\Http\Requests\UpdatePostRequest;
use App\Models\Post;
use Illuminate\Http\Response;

class PostController
{
    public function put(UpdatePostRequest $request, int $postId): Response
    {
        $post = Post::findOrFail($postId);

        $post->update($request->validated());

        return response()->noContent();
    }
}

Acontece que, no Laravel, os campos de um request são obtidos usando o método mágico __get() que consulta qualquer campo com o nome id no body request, query parameter e parâmetros de rotas. Então $this->id não é garantido de ser o parâmetro {id} na rota do endpoint, pode ser um ?id=x no query parameter ou {"id": x} no request body.

Ou seja: no controller é atualizado o recurso com o ID especificado no parâmetro da rota e no FormRequest é validado outro campo, também chamado id, só que vindo de outro lugar.

Com isso, era possível um atacante enviar uma requisição como PUT /posts/2 com um campo "id": 1 no body, onde: a validação para verificar se o usuário tinha permissão de acesso era feita com o ID 1 (o id no body), mas o recurso realmente atualizado no banco de dados tinha ID 2 (o id na rota).

Essa vulnerabilidade é conhecida como Broken Object Level Authentication (BOLA). No OWASP Top 10 de 2025 ela se enquadra na categoria Broken Access Control, sendo a categoria de vulnerabilidades mais comum em aplicações web.

Essa vulnerabilidade seria bem menos provável de acontecer se o Laravel não usasse métodos mágicos para obter os campos de input que, do ponto de vista de segurança, é um design bem ruim.

É uma vulnerabilidade bem simples e não tem nada de "uau" em ler sobre ela, mas ela serve para mostrar o que eu já falei em outro post: Usar framework não faz com que o seu projeto fique "seguro".

O $request->id é recurso documentado no framework, não foi invenção de moda do desenvolvedor. E por usar o que a documentação do Laravel diz que pode usar, uma vulnerabilidade ocorreu no código.

print-da-documentação-do-laravel-sobre-dynamic-properties

Carregando publicação patrocinada...