Desserialização insegura: como o ataque funciona e como se defender
Desserializar dados não confiáveis pode virar execução de código. Veja como o ataque funciona, onde ele mora e como desenhar uma defesa segura.
Neste artigo
Desserialização insegura é uma daquelas vulnerabilidades que parecem abstratas até você entender o que está realmente acontecendo, e então passam a parecer aterrorizantes. Ela figura há anos entre os riscos mais críticos de aplicações porque, nos piores casos, leva diretamente à execução de código no servidor, o troféu máximo para um atacante. E, ao contrário de outras falhas, ela costuma estar escondida em bibliotecas e mecanismos que o desenvolvedor usa sem perceber que está desserializando dado que veio de fora.
Este texto explica o que é serialização e desserialização, por que desserializar dado não confiável é tão perigoso, onde essa vulnerabilidade tende a morar nas aplicações e, sobretudo, como desenhar uma defesa que não dependa de sorte. A boa notícia é que a defesa fundamental é conceitualmente simples; a dificuldade está em enxergar todos os lugares onde o problema aparece.
O que é serialização e desserialização
Serialização é o processo de transformar uma estrutura de dados em memória, um objeto com seus campos e relações, numa sequência de bytes que pode ser guardada em disco, enviada pela rede ou colocada num cache. Desserialização é o caminho inverso: pegar aquela sequência de bytes e reconstruir o objeto em memória. É uma operação onipresente, porque programas precisam o tempo todo persistir estado, trocar mensagens entre serviços e guardar dados temporários.
O ponto crítico está na diferença entre formatos. Alguns formatos serializam apenas dados: números, textos, listas e mapas, como o JSON. Outros formatos serializam objetos, incluindo informação sobre quais classes instanciar e, em alguns casos, comportamento associado à reconstrução. É essa segunda categoria que abre a porta para o ataque, porque desserializar deixa de ser "ler dados" e passa a ser "reconstruir objetos arbitrários seguindo instruções contidas no próprio dado".
Por que desserializar dado não confiável é perigoso
Quando um mecanismo de desserialização reconstrói objetos a partir dos bytes recebidos, ele pode ser instruído a instanciar classes específicas e a preencher seus campos com valores específicos. Se o atacante controla os bytes, ele controla que objetos serão criados. O perigo não vem de um único objeto malicioso, mas de encadeamentos: o atacante combina objetos de classes que já existem na aplicação e em suas bibliotecas, de forma que a sequência de reconstrução e de métodos disparados automaticamente durante ela produza um efeito danoso. Essas combinações são chamadas de cadeias de gadgets.
O resultado, no pior cenário, é execução remota de código: o atacante monta um payload que, ao ser desserializado, executa comandos no servidor. Mas o dano não se limita a isso. Dependendo do mecanismo, é possível causar negação de serviço com estruturas que consomem memória ou CPU desproporcionalmente, adulterar a lógica da aplicação forjando objetos com estado que o servidor jamais criaria, e escalar privilégios manipulando objetos que representam sessão ou permissão. O fio condutor é sempre o mesmo: o dado deixou de ser passivo e virou instrução.
Onde a vulnerabilidade aparece nas linguagens
O problema não é de uma linguagem só; ele aparece em várias, em mecanismos diferentes com o mesmo espírito. Em Java, a serialização nativa de objetos é o exemplo histórico, e cadeias de gadgets em bibliotecas amplamente usadas tornaram o ataque prático por muito tempo. Em Python, o módulo de serialização de objetos por natureza executa código durante a reconstrução, o que faz de qualquer desserialização de dado não confiável nesse formato um risco direto. Em PHP, o mecanismo de reconstrução de objetos dispara métodos mágicos que podem ser encadeados. No ecossistema .NET, certos formatadores de objeto têm a mesma característica perigosa.
O detalhe importante é que você raramente escreve "desserializar objeto" de forma explícita e consciente. O mecanismo costuma estar embrulhado dentro de um framework, de uma biblioteca de cache, de um sistema de fila de mensagens, de um componente que guarda sessão. O desenvolvedor chama uma função de alto nível, ela aceita bytes e devolve um objeto pronto, e ninguém percebe que aqueles bytes vieram de uma fonte que o atacante consegue influenciar. Enxergar esse ponto é metade da defesa.
Onde o dado não confiável entra
Para explorar, o atacante precisa que os bytes que serão desserializados passem por um caminho que ele controla. Os candidatos são muitos. Cookies que guardam estado serializado do lado do cliente são um clássico, porque o cliente pode alterá-los. Tokens e parâmetros que carregam um blob serializado. Caches e sessões que, se o atacante conseguir escrever neles por outra via, viram um vetor. Filas de mensagens em que um produtor comprometido injeta payloads que um consumidor confiante desserializa. Campos ocultos de formulário, cabeçalhos, uploads de arquivo que contêm objetos serializados.
O sintoma de que você pode estar exposto é encontrar, em qualquer fronteira por onde entra dado de fora, um formato binário ou opaco que a aplicação reconstrói em objeto. Se você vê um valor que "não é JSON legível" sendo transformado de volta em estrutura rica, acenda o alerta. Manter um inventário dessas fronteiras conversa diretamente com uma boa gestão de vulnerabilidades e CVEs, já que muitos ataques de desserialização exploram gadgets em versões vulneráveis de bibliotecas conhecidas.
Como se defender
A defesa contra desserialização insegura tem uma regra soberana e um conjunto de reforços. A regra soberana é a mais eficaz e deve ser buscada sempre que possível.
Não desserialize dado não confiável em formato que instancia objetos
A defesa que resolve o problema pela raiz é nunca desserializar dado vindo de fonte não confiável usando um mecanismo que reconstrói objetos arbitrários. Se você precisa receber dados do cliente ou de outra fronteira que o atacante toca, use um formato de dados puros, como JSON, que representa apenas números, textos, listas e mapas, e reconstrua você mesmo os objetos do seu domínio a partir desses dados, validando cada campo. Assim, mesmo que o atacante controle o conteúdo, ele controla apenas dados, não a escolha de classes a instanciar nem o disparo de comportamento. Essa mudança de formato elimina a categoria inteira de gadget chains.
Valide contra um schema e trate tudo como hostil
Trocar de formato não dispensa validação. Todo dado que entra deve passar por um schema explícito que define os campos esperados, seus tipos e seus limites, rejeitando qualquer coisa fora do previsto. Trate a fronteira como hostil: valide na borda, converta explicitamente, e só então deixe o dado circular na aplicação. Esse rigor de borda é a mesma disciplina que previne outras classes de injeção, como a injeção de SQL, e vale exatamente pelo mesmo motivo, não confiar em nada que o cliente envia.
Verifique integridade quando não der para evitar
Existem situações em que trocar o mecanismo não é viável de imediato, por exemplo com um formato serializado já em uso entre serviços seus. Nesse caso, garanta a integridade e a autenticidade do dado: assine o blob com um segredo que só o servidor conhece e verifique a assinatura antes de desserializar, recusando qualquer payload que não bata. Isso impede que o atacante forje um payload malicioso, embora não proteja contra um serviço interno comprometido, então continue tratando a mudança de formato como a meta.
Isole, restrinja e mantenha atualizado
Adote defesa em profundidade. Rode o componente que desserializa com o menor privilégio possível, num contexto isolado, de forma que uma execução de código bem-sucedida encontre um ambiente pobre e contido em vez de acesso total. Restrinja, quando o mecanismo permitir, quais classes podem ser instanciadas a uma allowlist mínima. E mantenha bibliotecas e runtime atualizados, porque muitos ataques dependem de gadgets presentes em versões antigas de dependências; atualizar remove gadgets do alcance do atacante. Nenhuma dessas medidas substitui a regra soberana, mas todas reduzem o dano quando algo escapa.
Como reconhecer o problema no seu código
Uma dificuldade prática dessa vulnerabilidade é que ela raramente se anuncia. Ninguém escreve um comentário dizendo "aqui eu desserializo dado do atacante". Ela se esconde em chamadas de aparência inocente, e por isso vale desenvolver o hábito de reconhecer os sinais. O primeiro sinal é a presença, em qualquer fronteira de entrada, de um valor opaco que não é texto legível nem JSON, mas que a aplicação transforma de volta numa estrutura rica de objetos. Se você encontra um blob binário ou codificado que vira um objeto do seu domínio, pergunte de onde ele veio e quem pode influenciá-lo.
O segundo sinal está nas dependências. Muitas bibliotecas de conveniência oferecem funções de "salvar e carregar objeto" que, por baixo, usam mecanismos de serialização perigosos. Cache que guarda objetos, filas que transportam mensagens ricas, componentes que persistem sessão do lado do cliente: todos podem estar desserializando por dentro sem que a interface deixe isso óbvio. Ler a documentação do que essas bibliotecas fazem com os bytes, e verificar se elas usam um formato de dados puros ou um formato que instancia objetos, é parte da diligência.
O terceiro sinal é arquitetural. Sempre que dois componentes trocam objetos serializados em vez de dados, especialmente se um deles toca uma fronteira externa, existe risco latente. A recomendação é mapear essas trocas e classificá-las por confiança: quais bytes vêm de fonte que o atacante alcança, e quais vêm apenas de componentes internos que você controla por inteiro. Os primeiros exigem a migração para formato de dados puros com schema; os segundos merecem ao menos a assinatura de integridade e vigilância sobre as versões das bibliotecas envolvidas. Fazer esse inventário com honestidade costuma revelar mais pontos de desserialização não confiável do que o time imaginava ter, e é o passo que transforma uma preocupação abstrata numa lista concreta de lugares a corrigir.
Conclusão
Desserialização insegura é perigosa porque transforma dado em instrução: o que deveria ser uma leitura passiva de bytes vira a reconstrução de objetos que o atacante escolhe, e no limite a execução de código. A vulnerabilidade se esconde em cookies, tokens, caches, filas e componentes que desserializam sem que o desenvolvedor perceba. A defesa central é direta e você deve persegui-la com afinco: não desserialize dado não confiável em formatos que instanciam objetos; use dados puros com schema validado. Em volta disso, some verificação de integridade onde a troca não for possível, isolamento, menor privilégio, restrição de classes e atualização de dependências. Quem trata toda fronteira de entrada como potencialmente hostil e recusa reconstruir objetos a partir de bytes de origem duvidosa fecha a porta que essa classe de ataque tenta abrir.
