7

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...
3

Sua publicação me fez pensar um pouco sobre o mecanismo e percebi algo observando o trecho do código que você pegou no projeto: quem desenvolveu não soube usar o próprio mecanismo de acesso a parâmetros da rota que o Laravel tem, o $this->route('parameter'). Isso pode ter acontecido por falta de atenção ou até mesmo por falta do conhecimento.

Na própria documentação (Validation - Laravel 13.x) é citado sobre isso:

[...] Also, note the call to the route method in the example above. This method grants you access to the URI parameters defined on the route being called [...]

O uso de métodos mágicos realmente é uma faca de dois gumes, mas este caso citado em específico foi por um uso tecnicamente incorreto, apesar do método mágico também funcionar para parâmetros da rota. Onde costumo ver usarem mais métodos mágicos é na obtenção de um input repassado via parâmetros query ou no body da requisição.

Apesar da publicação ser sobre a questão de validação de inputs e não sobre o Laravel em si (sendo este apenas o exemplo do caso ocorrido), vale a informação.

PS.: Também não gosto muito do uso de métodos mágicos para inputs e parâmetros no Laravel. Aprendi sobre isso no início, mas hoje faço mais uso do método $this->input() para pegar um input dos parâmetros query ou do body (principalmente por facilitar a definição de valor padrão caso o input não exista). Sempre usei o $this->route() para pegar o valor de um parâmetro da url da requisição.

1

Boa! Realmente, o Laravel tem métodos que pegam parâmetros de entradas específicas. Só o dynamic properties que acaba sendo meio zoado.

O uso de métodos mágicos realmente é uma faca de dois gumes, mas este caso citado em específico foi por um uso tecnicamente incorreto, apesar do método mágico também funcionar para parâmetros da rota. Onde costumo ver usarem mais métodos mágicos é na obtenção de um input repassado via parâmetros query ou no body da requisição.

No geral, qualquer recurso de qualquer linguagem que não tem uma funcionalidade determística, no sentido de que sempre irá fazer a mesma coisa independentemente do contexto ou estado, costumam causar problemas; e em muitos casos falhas de segurança (como é o caso do post).

É sempre bom evitar fazer ou usar funções não-determinísticas, exceto quando o não-determinismo é o propósito da função (como uma função que retorna um número aleatório).

1

Acredito que a correção fique assim:
`public function authorize(): bool
{
post = Post::find(this->route('postId')); // parâmetro de rota, não input
if (!$post)return false;

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

}

`
Assim os dois métodos validam e operam no mesmo recurso.