CodeGym /Cursos /JAVA 25 SELF /Segurança da serialização: práticas recomendadas

Segurança da serialização: práticas recomendadas

JAVA 25 SELF
Nível 43 , Lição 4
Disponível

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.

1
Pesquisa/teste
Configuração de serialização, nível 43, lição 4
Indisponível
Configuração de serialização
Configuração de serialização
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION