JavaRush /Blogue Java /Random-PT /Segurança em Java: melhores práticas
Roman Beekeeper
Nível 35

Segurança em Java: melhores práticas

Publicado no grupo Random-PT
Nas aplicações de servidor, um dos indicadores mais importantes é a segurança. Este é um dos tipos de Requisitos Não Funcionais . Segurança em Java: melhores práticas - 1A segurança tem muitos componentes. Claro, para cobrir totalmente todos os princípios e ações de proteção que são conhecidos, você precisa escrever mais de um artigo, então vamos nos concentrar no mais importante. Uma pessoa bem versada neste tema, que seja capaz de configurar todos os processos e garantir que não criem novas falhas de segurança, será necessária em qualquer equipe. Claro, você não deve pensar que, se seguir essas práticas, o aplicativo estará totalmente seguro. Não! Mas com certeza será mais seguro com eles. Ir.

1. Fornece segurança no nível da linguagem Java

Em primeiro lugar, a segurança em Java começa logo no nível dos recursos da linguagem. Isto é o que faríamos se não existissem modificadores de acesso?... Anarquia, nada menos. Uma linguagem de programação nos ajuda a escrever código seguro e também a aproveitar muitos recursos de segurança implícitos:
  1. Digitação forte. Java é uma linguagem de tipo estaticamente que fornece a capacidade de detectar erros de tipo em tempo de execução.
  2. Modificadores de acesso. Graças a eles podemos configurar o acesso às classes, métodos e campos de classe da maneira que necessitamos.
  3. Gerenciamento automático de memória. Para isso, nós (Javaístas ;)) temos o Garbage Collector, que dispensa a configuração manual. Sim, às vezes surgem problemas.
  4. Verificação de bytecode: Java compila em bytecode, que é verificado pelo tempo de execução antes de executá-lo.
Entre outras coisas, há recomendações da Oracle sobre segurança. Claro, não está escrito em “alto estilo” e você pode adormecer várias vezes enquanto lê, mas vale a pena. Um documento particularmente importante é o Secure Coding Guidelines for Java SE , que fornece dicas sobre como escrever código seguro. Este documento contém muitas informações úteis. Se possível, definitivamente vale a pena ler. Para despertar o interesse por esse material, aqui vão algumas dicas interessantes:
  1. Evite serializar classes sensíveis à segurança. Nesse caso, você pode obter a interface da classe do arquivo serializado, sem falar nos dados que estão sendo serializados.
  2. Tente evitar classes de dados mutáveis. Isso oferece todos os benefícios das classes imutáveis ​​(por exemplo, segurança de thread). Se houver um objeto mutável, isso poderá levar a um comportamento inesperado.
  3. Faça cópias de objetos mutáveis ​​retornados. Se um método retornar uma referência a um objeto mutável interno, o código do cliente poderá alterar o estado interno do objeto.
  4. E assim por diante…
Em geral, as Diretrizes de codificação segura para Java SE contêm um conjunto de dicas e truques sobre como escrever código em Java de maneira correta e segura.

2. Elimine a vulnerabilidade de injeção de SQL

Vulnerabilidade única. A sua singularidade reside no facto de ser uma das vulnerabilidades mais famosas e uma das mais comuns. Se você não está interessado na questão da segurança, não saberá disso. O que é injeção de SQL? Este é um ataque a um banco de dados através da injeção de código SQL adicional onde não é esperado. Digamos que temos um método que utiliza algum parâmetro para consultar o banco de dados. Por exemplo, nome de usuário. O código com a vulnerabilidade será mais ou menos assim:
// Метод достает из базы данных всех пользователей с определенным именем
public List<User> findByFirstName(String firstName) throws SQLException {
   // Создается связь с базой данных
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);

   // Пишем sql request в базу данных с нашим firstName
   String query = "SELECT * FROM USERS WHERE firstName = " + firstName;

   // выполняем request
   Statement statement = connection.createStatement();
   ResultSet result = statement.executeQuery(query);

   // при помощи mapToUsers переводит ResultSet в коллекцию юзеров.
   return mapToUsers(result);
}

private List<User> mapToUsers(ResultSet resultSet) {
   //переводит в коллекцию юзеров
}
Neste exemplo, a consulta SQL é preparada antecipadamente em uma linha separada. Parece que qual é o problema, certo? Talvez o problema seja que seria melhor usar String.format? Não? E então? Vamos nos colocar no lugar de um testador e pensar no que pode ser transmitido no valor firstName. Por exemplo:
  1. Você pode passar o que é esperado – o nome de usuário. Então o banco de dados retornará todos os usuários com esse nome.
  2. Você pode passar uma string vazia: então todos os usuários serão retornados.
  3. Ou você pode passar o seguinte: “''; DROP TABLE USUÁRIOS;”. E aqui haverá problemas maiores. Esta consulta removerá a tabela do banco de dados. Com todos os dados. TODOS.
Você pode imaginar quais problemas isso pode causar? Então você pode escrever o que quiser. Você pode alterar o nome de todos os usuários, pode excluir seus endereços. O espaço para sabotagem é vasto. Para evitar isso, você precisa parar de injetar uma consulta pronta e, em vez disso, construí-la usando parâmetros. Esta deve ser a única maneira de consultar o banco de dados. Desta forma você pode eliminar esta vulnerabilidade. Exemplo:
// Метод достает из базы данных всех пользователей с определенным именем
public List<User> findByFirstName(String firstName) throws SQLException {
   // Создается связь с базой данных
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);

   // Создаем параметризированный request.
   String query = "SELECT * FROM USERS WHERE firstName = ?";

   // Создаем подготовленный стейтмент с параметризованным requestом
   PreparedStatement statement = connection.prepareStatement(query);

   // Передаем meaning параметра
   statement.setString(1, firstName);

   // выполняем request
   ResultSet result = statement.executeQuery(query);

   // при помощи mapToUsers переводим ResultSet в коллекцию юзеров.
   return mapToUsers(result);
}

private List<User> mapToUsers(ResultSet resultSet) {
   //переводим в коллекцию юзеров
}
Dessa forma, essa vulnerabilidade é evitada. Para quem quiser se aprofundar neste artigo, aqui está um ótimo exemplo . Como você sabe se entendeu essa parte? Se a piada abaixo ficou clara, então este é um sinal claro de que a essência da vulnerabilidade é clara :D Segurança em Java: melhores práticas - 2

3. Verifique e mantenha as dependências atualizadas

O que isso significa? Para quem não sabe o que é dependência, vou explicar: é um arquivo jar com código que é conectado a um projeto usando sistemas de construção automática (Maven, Gradle, Ant) para reutilizar a solução de outra pessoa. Por exemplo, Projeto Lombok , que gera getters, setters, etc. para nós em tempo de execução. E se falamos de aplicativos grandes, eles usam muitas dependências diferentes. Algumas são transitivas (ou seja, cada dependência pode ter suas próprias dependências e assim por diante). Portanto, os invasores estão cada vez mais atentos às dependências de código aberto, uma vez que são usadas regularmente e podem causar problemas para muitos clientes. É importante ter certeza de que não há vulnerabilidades conhecidas em toda a árvore de dependências (que é exatamente o que parece). E existem várias maneiras de fazer isso.

Use Snyk para monitoramento

A ferramenta Snyk verifica todas as dependências do projeto e sinaliza vulnerabilidades conhecidas. Lá você pode cadastrar e importar seus projetos via GitHub, por exemplo. Segurança em Java: melhores práticas - 3Além disso, como você pode ver na imagem acima, se uma versão mais recente tiver uma solução para esta vulnerabilidade, Snyk se oferecerá para fazê-lo e criará uma solicitação pull. Ele pode ser usado gratuitamente para projetos de código aberto. Os projetos serão verificados com alguma frequência: uma vez por semana, uma vez por mês. Cadastrei-me e adicionei todos os meus repositórios públicos ao scan do Snyk (não há nada de perigoso nisso: eles já estão abertos a todos). A seguir, Snyk mostrou o resultado da varredura: Segurança em Java: melhores práticas - 4E depois de um tempo, o Snyk-bot preparou vários Pull-Requests em projetos onde as dependências precisam ser atualizadas: Segurança em Java: melhores práticas - 5E aqui está outro: Segurança em Java: melhores práticas - 6Então esta é uma excelente ferramenta para busca de vulnerabilidades e monitoramento para atualização novas versões.

Use o laboratório de segurança do GitHub

Aqueles que trabalham no GitHub também podem aproveitar suas ferramentas integradas. Você pode ler mais sobre essa abordagem na minha tradução do blog Anúncio do GitHub Security Lab . Essa ferramenta, é claro, é mais simples que o Snyk, mas você definitivamente não deve negligenciá-la. Além disso, o número de vulnerabilidades conhecidas só aumentará, então tanto o Snyk quanto o GitHub Security Lab irão se expandir e melhorar.

Ativar Sonatype DepShield

Se você usa GitHub para armazenar seus repositórios, você pode adicionar um dos aplicativos do MarketPlace aos seus projetos - Sonatype DepShield. Com sua ajuda, você também pode verificar se há dependências em projetos. Além disso, caso encontre algo, será criado um Issue GitHub com uma descrição correspondente, conforme mostrado abaixo: Segurança em Java: melhores práticas - 7

4. Manuseie dados confidenciais com cuidado

Segurança em Java: melhores práticas - 8Na língua inglesa, a frase “dados confidenciais” é mais comum. A divulgação de informações pessoais, números de cartão de crédito e outras informações pessoais do cliente pode causar danos irreparáveis. Primeiro de tudo, você precisa examinar atentamente o design do aplicativo e determinar se algum dado é realmente necessário. Talvez não haja necessidade de alguns deles, mas foram acrescentados para um futuro que não chegou e que dificilmente chegará. Além disso, durante o registro do projeto, esses dados podem vazar. Uma maneira simples de evitar que dados confidenciais entrem em seus logs é limpar os métodos toString()das entidades do domínio (como Usuário, Aluno, Professor e assim por diante). Isso evitará que campos confidenciais sejam impressos acidentalmente. Se você usar o Lombok para gerar um método toString(), poderá usar uma anotação @ToString.Excludepara evitar que o campo seja usado na saída por meio do método toString(). Além disso, tenha muito cuidado ao compartilhar dados com o mundo exterior. Por exemplo, existe um endpoint http que mostra os nomes de todos os usuários. Não há necessidade de mostrar o ID exclusivo interno do usuário. Por que? Porque com ele um invasor pode obter outras informações mais confidenciais sobre cada usuário. Por exemplo, se você usar Jackson para serializar e desserializar POJOs em JSON , poderá usar as anotações @JsonIgnoree @JsonIgnorePropertiespara evitar que campos específicos sejam serializados e desserializados. Em geral, você precisa usar classes POJO diferentes para locais diferentes. O que isso significa?
  1. Para trabalhar com o banco de dados, utilize apenas POJO - Entidade.
  2. Para trabalhar com lógica de negócios, transfira Entidade para Modelo.
  3. Para trabalhar com o mundo exterior e enviar solicitações http, utilize entidades terceiras - DTO.
Desta forma você pode definir claramente quais campos serão visíveis externamente e quais não.

Use criptografia forte e algoritmos de hash

Os dados confidenciais do cliente devem ser armazenados de forma segura. Para fazer isso, você precisa usar criptografia. Dependendo da tarefa, você precisa decidir que tipo de criptografia usar. Além disso, a criptografia mais forte leva mais tempo, então, novamente, você precisa considerar o quanto a necessidade justifica o tempo gasto nela. Claro, você mesmo pode escrever o algoritmo. Mas isso é desnecessário. Você pode aproveitar as soluções existentes nesta área. Por exemplo, Google Tink :
<!-- https://mvnrepository.com/artifact/com.google.crypto.tink/tink -->
<dependency>
   <groupId>com.google.crypto.tink</groupId>
   <artifactId>tink</artifactId>
   <version>1.3.0</version>
</dependency>
Vamos ver como utilizá-lo, usando o exemplo de como criptografar de uma forma e de outra:
private static void encryptDecryptExample() {
   AeadConfig.register();
   KeysetHandle handle = KeysetHandle.generateNew(AeadKeyTemplates.AES128_CTR_HMAC_SHA256);

   String plaintext = "Цой жив!";
   String aad = "Юрий Клинских";

   Aead aead = handle.getPrimitive(Aead.class);
   byte[] encrypted = aead.encrypt(plaintext.getBytes(), aad.getBytes());
   String encryptedString = Base64.getEncoder().encodeToString(encrypted);
   System.out.println(encryptedString);

   byte[] decrypted = aead.decrypt(Base64.getDecoder().decode(encrypted), aad.getBytes());
   System.out.println(new String(decrypted));
}

Criptografia de senha

Para esta tarefa, é mais seguro usar criptografia assimétrica. Por que? Porque o aplicativo realmente não precisa descriptografar as senhas. Esta é a abordagem geral. Na realidade, quando um usuário insere uma senha, o sistema a criptografa e a compara com o que está no cofre de senhas. A criptografia é realizada pelos mesmos meios, então você pode esperar que eles correspondam (se você inserir a senha correta;), é claro). BCrypt e SCrypt são adequados para esta finalidade. Ambas são funções unidirecionais (hashes criptográficos) com algoritmos computacionalmente complexos que levam muito tempo. É exatamente disso que você precisa, pois decifrá-lo de frente levará uma eternidade. Por exemplo, Spring Security oferece suporte a uma variedade de algoritmos. SCryptPasswordEncoderVocê também pode usar BCryptPasswordEncoder. O que é um algoritmo de criptografia forte agora pode ser fraco no próximo ano. Como resultado, concluímos que é necessário verificar os algoritmos utilizados e atualizar as bibliotecas com algoritmos.

Em vez de saída

Hoje falamos sobre segurança e, claro, muitas coisas ficaram em segundo plano. Acabei de abrir a porta para um novo mundo para você: um mundo que vive sua própria vida. Com a segurança é o mesmo que com a política: se você não se envolver na política, a política se envolverá em você. Tradicionalmente, sugiro assinar minha conta no Github . Lá posto meus trabalhos sobre diversas tecnologias que estudo e aplico no trabalho.

Links Úteis

Sim, quase todos os artigos do site são escritos em inglês. Quer queiramos ou não, o inglês é a língua de comunicação dos programadores. Todos os artigos, livros e revistas mais recentes sobre programação são escritos em inglês. É por isso que meus links para recomendações estão principalmente em inglês:
  1. Habr: Injeção SQL para Iniciantes
  2. Oracle: Centro de Recursos de Segurança Java
  3. Oracle: Diretrizes de codificação segura para Java SE
  4. Baeldung: Os princípios básicos da segurança Java
  5. Médio: 10 dicas para aumentar sua segurança Java
  6. Snyk: 10 práticas recomendadas de segurança java
  7. JR: Anúncio do GitHub Security Lab: protegendo todo o seu código juntos
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION