1. Práticas recomendadas para serialização segura
Serialização — é como embalar a bagagem no aeroporto: se você não sabe o que há dentro nem a quem entrega a mala, pode ter uma surpresa desagradável no controle de segurança. Em Java, a serialização permite salvar e restaurar objetos com facilidade, mas abre a porta para uma série de ataques quando os dados vêm de fontes não confiáveis.
Ameaça clássica:
A serialização em Java pode ser insegura. Se um invasor fornecer um fluxo malicioso, durante a desserialização podem ocorrer consequências indesejadas: desde a alteração de campos até a execução de código não desejado. Isso não é um conto de terror do manual — na história do Java realmente houve casos em que ataques se basearam nesse mecanismo.
Por que isso acontece?
O fato é que desserializar não é apenas restaurar valores de campos. No processo, um objeto completo é criado: métodos especiais podem ser chamados (por exemplo, readObject, readResolve) e, às vezes, pontos vulneráveis no código podem ser atingidos via reflexão. Classes de bibliotecas de terceiros são especialmente perigosas: algumas delas executam ações já na fase de desserialização. Portanto, nunca confie em dados serializados vindos de fora.
Use transient para dados sensíveis
Se a sua classe tiver campos que armazenam senhas, tokens, chaves privadas ou outras informações sensíveis, declare-os como transient. Esses dados não serão incluídos no fluxo serializado.
import java.io.Serializable;
public class User implements Serializable {
private String username;
private transient String password; // não é serializado
// ...construtores, getters, setters...
}
O que acontece na desserialização? O campo password terá o valor padrão (null para strings). Isso é bom: senhas não serão armazenadas em arquivos nem enviadas pela rede.
Defina serialVersionUID explicitamente
Sempre indique o serialVersionUID explicitamente. Isso reduz a probabilidade de erros de compatibilidade e minimiza o risco de substituição de classes na desserialização.
private static final long serialVersionUID = 1L;
Por que isso é importante para a segurança? Se você não definir o serialVersionUID, o compilador o gerará automaticamente com base na estrutura da classe. Isso pode levar a incompatibilidades inesperadas e, em teoria, a abusos com a substituição de classes com o mesmo nome, mas outra estrutura.
Verifique os tipos dos objetos na desserialização
Não confie no que veio da rede ou de um arquivo. Após desserializar, sempre verifique se o objeto obtido tem o tipo esperado antes de usá-lo.
Object obj = objectInputStream.readObject();
if (obj instanceof User) {
User user = (User) obj;
// trabalhamos com user com segurança
} else {
// tipo inesperado — lançar uma exceção ou tratar o erro
}
Por que isso é necessário? Um fluxo malicioso pode conter um objeto de outra classe que implementa Serializable, mas não corresponde à sua lógica de negócio.
Restrinja as classes que podem ser desserializadas (ObjectInputFilter)
A partir do Java 9, use filtros — ObjectInputFilter — para limitar o conjunto de classes que é permitido desserializar. É como um controle de acesso na entrada.
Exemplo: configuração do filtro
import java.io.*;
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.User;com.example.Address;!*"
);
ObjectInputStream in = new ObjectInputStream(inputStream);
in.setObjectInputFilter(filter);
Object obj = in.readObject(); // agora apenas User e Address são desserializados
Esse filtro permite apenas as classes User e Address do seu aplicativo. Todas as outras serão bloqueadas — uma exceção será lançada. Isso reduz significativamente o risco de entrada de um objeto malicioso.
Não desserialize dados de fontes não confiáveis
Regra de ouro: se você não confia na fonte dos dados — não desserialize. Prefira formatos que não executam código ao serem analisados (por exemplo, JSON, XML com parsers seguros).
Exemplo de má prática:
// Nunca faça isso com dados da internet!
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Object obj = in.readObject(); // perigoso!
O que fazer em vez disso?
- Use parsers JSON (por exemplo, Gson/Jackson) ou parsers XML com validação.
- Se a serialização binária for necessária — filtre as classes via ObjectInputFilter e verifique os tipos (instanceof).
Use formatos alternativos para integração com sistemas externos
Para integrações, use formatos que não executam código durante a análise: JSON, XML, Protocol Buffers etc. Isso praticamente elimina ataques via desserialização.
// Em vez de ObjectInputStream, use um parser JSON
User user = gson.fromJson(jsonString, User.class);
Não armazene objetos serializados em locais públicos
Arquivos com objetos serializados podem conter dados sensíveis. Não os armazene em diretórios públicos e restrinja as permissões no nível do sistema de arquivos.
Não confie na serialização para verificar a integridade
A serialização não garante a integridade ou a autenticidade dos dados. Use assinaturas digitais, somas de verificação ou criptografia quando alterações não forem aceitáveis.
2. Prática: exemplo de ObjectInputFilter e demonstração de vulnerabilidade
Exemplo de filtragem de classes
Suponha que temos a classe User:
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password;
// ...construtores, getters, setters...
}
O filtro permite apenas User:
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.User;!*"
);
in.setObjectInputFilter(filter);
Agora, se alguém tentar inserir um objeto de outra classe, a desserialização terminará com erro.
Demonstração de uma potencial vulnerabilidade
Classe maliciosa:
// Imagine que alguém inseriu uma classe assim
public class Evil implements java.io.Serializable {
static {
System.out.println("Código malicioso executado!");
// aqui pode haver qualquer coisa...
}
}
Se você não filtrar as classes, durante a desserialização um objeto Evil pode ser criado, e o inicializador estático será executado ao carregar a classe — isso já é um ataque real.
4. Erros típicos ao garantir a segurança da serialização
Erro nº 1: Desserialização sem filtragem e verificação de tipo. Com frequência, desenvolvedores leem um objeto do fluxo e imediatamente fazem cast para o tipo desejado. Isso abre a porta para ataques. Use ObjectInputFilter e verifique o tipo com instanceof.
Erro nº 2: Armazenar dados sensíveis sem transient. Se você esquecer de declarar senhas/chaves como transient, elas irão para o fluxo e podem vazar junto com o arquivo.
Erro nº 3: Ausência de serialVersionUID. Sem um serialVersionUID explícito, são possíveis erros inesperados de compatibilidade e riscos associados à substituição de classes.
Erro nº 4: Usar serialização para integração com sistemas externos. A serialização binária é conveniente dentro do aplicativo (por exemplo, cache), mas é perigosa para troca externa. Prefira JSON/XML/Proto com parsers seguros.
Erro nº 5: Ignorar a integridade dos dados. Alterações nos bytes de um arquivo serializado podem passar despercebidas. Aplique assinaturas digitais, somas de verificação ou criptografia.
GO TO FULL VERSION