CodeGym /Blog Java /Random-ES /Seguridad en Java: mejores prácticas
John Squirrels
Nivel 41
San Francisco

Seguridad en Java: mejores prácticas

Publicado en el grupo Random-ES
Una de las métricas más importantes en las aplicaciones de servidor es la seguridad. Este es un tipo de requisito no funcional . Seguridad en Java: mejores prácticas - 1La seguridad incluye muchos componentes. Por supuesto, se necesitaría más de un artículo para cubrir completamente todos los principios y medidas de seguridad conocidos, por lo que nos detendremos en los más importantes. Una persona bien versada en este tema puede configurar todos los procesos relevantes, evitar crear nuevos agujeros de seguridad y será necesaria en cualquier equipo. Por supuesto, no debe pensar que su aplicación será 100% segura si sigue estas prácticas. ¡No! Pero definitivamente será más seguro con ellos. Vamos.

1. Proporcionar seguridad a nivel del lenguaje Java

En primer lugar, la seguridad en Java comienza justo en el nivel de las capacidades del lenguaje. ¿Qué haríamos si no hubiera modificadores de acceso? No habría nada más que anarquía. El lenguaje de programación nos ayuda a escribir código seguro y también hace uso de muchas funciones de seguridad implícitas:
  1. Mecanografía fuerte. Java es un lenguaje de tipo estático. Esto hace posible detectar errores relacionados con el tipo en tiempo de ejecución.
  2. Modificadores de acceso. Estos nos permiten personalizar el acceso a clases, métodos y campos según sea necesario.
  3. Gestión automática de memoria. Para ello, los desarrolladores de Java cuentan con un recolector de basura que nos libera de tener que configurar todo manualmente. Sí, a veces surgen problemas.
  4. Verificación de código de bytes : Java se compila en código de bytes, que el tiempo de ejecución verifica antes de ejecutarlo.
Además, están las recomendaciones de seguridad de Oracle . Por supuesto, no está escrito en un lenguaje elevado y es posible que te duermas varias veces mientras lo lees, pero vale la pena. En particular, el documento titulado Directrices de codificación segura para Java SE es importante. Proporciona consejos sobre cómo escribir código seguro. Este documento transmite una enorme cantidad de información de gran utilidad. Si tienes la oportunidad, definitivamente deberías leerlo. Para despertar su interés en este material, aquí hay algunos consejos interesantes:
  1. Evite serializar clases sensibles a la seguridad. La serialización expone la interfaz de clase en el archivo serializado, sin mencionar los datos que se serializan.
  2. Intente evitar las clases mutables para los datos. Esto proporciona todos los beneficios de las clases inmutables (por ejemplo, seguridad de subprocesos). Si tiene un objeto mutable, puede provocar un comportamiento inesperado.
  3. Haga copias de los objetos mutables devueltos. Si un método devuelve una referencia a un objeto mutable interno, el código del cliente podría cambiar el estado interno del objeto.
  4. Etcétera…
Básicamente, las Directrices de codificación segura para Java SE son una colección de consejos y trucos sobre cómo escribir código Java de forma correcta y segura.

2. Eliminar las vulnerabilidades de inyección SQL

Este es un tipo especial de vulnerabilidad. Es especial porque es tanto una de las vulnerabilidades más famosas como una de las más comunes. Si nunca te ha interesado la seguridad informática, entonces no sabrás nada al respecto. ¿Qué es la inyección SQL? Este es un ataque a la base de datos que consiste en inyectar código SQL adicional donde no se espera. Supongamos que tenemos un método que acepta algún tipo de parámetro para consultar la base de datos. Por ejemplo, un nombre de usuario. El código vulnerable se vería así:

// This method retrieves from the database all users with a certain name
public List findByFirstName(String firstName) throws SQLException {
   // Connect to the database
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);
  
   // Compose a SQL database query with our firstName
   String query = "SELECT * FROM USERS WHERE firstName = " + firstName;
  
   // Execute the query
   Statement statement = connection.createStatement();
   ResultSet result = statement.executeQuery(query);

   // Use mapToUsers to convert the ResultSet into a collection of users.
   return mapToUsers(result);
}

private List mapToUsers(ResultSet resultSet) {
   // Converts to a collection of users
}
En este ejemplo, una consulta SQL se prepara de antemano en una línea separada. Entonces, ¿cuál es el problema, verdad? ¿Quizás el problema es que sería mejor usar String.format ? ¿No? Bueno, ¿entonces qué? Pongámonos en el lugar de un probador y pensemos qué se podría pasar como el valor de firstName . Por ejemplo:
  1. Podemos pasar lo que se espera: un nombre de usuario. Luego, la base de datos devolverá todos los usuarios con ese nombre.
  2. Podemos pasar una cadena vacía. Entonces todos los usuarios serán devueltos.
  3. Pero también podemos pasar lo siguiente: "'; DROP TABLE USERS;". Y aquí tenemos ahora enormes problemas. Esta consulta eliminará una tabla de la base de datos. Junto con todos los datos. TODO ELLO.
¿Te imaginas los problemas que esto causaría? Más allá de eso, puedes escribir lo que quieras. Puede cambiar los nombres de todos los usuarios. Puede eliminar sus direcciones. El alcance del sabotaje es inmenso. Para evitar esto, debe evitar la inyección de una consulta preparada y, en su lugar, formar la consulta utilizando parámetros. Esta debería ser la única forma de crear consultas de base de datos. Así es como puedes eliminar esta vulnerabilidad. Por ejemplo:

// This method retrieves from the database all users with a certain name
public List findByFirstName(String firstName) throws SQLException {
   // Connect to the database
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);

   // Create a parameterized query.
   String query = "SELECT * FROM USERS WHERE firstName = ?";

   // Create a prepared statement with the parameterized query
   PreparedStatement statement = connection.prepareStatement(query);
  
   // Pass the parameter's value
   statement.setString(1, firstName);

   // Execute the query
   ResultSet result = statement.executeQuery(query);

   // Use mapToUsers to convert the ResultSet into a collection of users.
   return mapToUsers(result);
}

private List mapToUsers(ResultSet resultSet) {
   // Converts to a collection of users
}
De esta forma se evita la vulnerabilidad. Para aquellos que quieran profundizar más en este artículo, aquí hay un gran ejemplo . ¿Cómo sabes cuándo comprendes esta vulnerabilidad? Si entiende la broma en el cómic a continuación, entonces probablemente tenga una idea clara de qué se trata esta vulnerabilidad: DSeguridad en Java: mejores prácticas - 2

3. Escanea las dependencias y mantenlas actualizadas

¿Qué significa eso? Si no sabes lo que es una dependencia, te lo explico. Una dependencia es un archivo JAR con código que está conectado a un proyecto mediante sistemas de compilación automáticos (Maven, Gradle, Ant) para reutilizar la solución de otra persona. Por ejemplo, Project Lombok , que genera getters, setters, etc. para nosotros en el tiempo de ejecución. Las aplicaciones grandes pueden tener montones, montones de dependencias. Algunos son transitivos (es decir, cada dependencia puede tener sus propias dependencias, etc.). Como resultado, los atacantes prestan cada vez más atención a las dependencias de código abierto, ya que se utilizan con regularidad y muchos clientes pueden tener problemas a causa de ellas. Es importante asegurarse de que no haya vulnerabilidades conocidas en todo el árbol de dependencias (sí, parece un árbol). Hay varias formas de hacer esto.

Use Snyk para monitorear dependencias

Snyk verifica todas las dependencias del proyecto y señala las vulnerabilidades conocidas. Puede registrarse en Snyk e importar sus proyectos a través de GitHub. Seguridad en Java: mejores prácticas - 3Además, como puede ver en la imagen de arriba, si se soluciona una vulnerabilidad en una versión más nueva, Snyk ofrecerá la solución y creará una solicitud de extracción. Puede usarlo de forma gratuita para proyectos de código abierto. Los proyectos se escanean a intervalos regulares, por ejemplo, una vez a la semana, una vez al mes. Me registré y agregué todos mis repositorios públicos al escaneo de Snyk (esto no tiene nada de peligroso, ya que ya son públicos para todos). Snyk luego mostró el resultado del escaneo: Seguridad en Java: mejores prácticas - 4Y después de un tiempo, Snyk-bot preparó varias solicitudes de extracción en proyectos donde las dependencias deben actualizarse: Seguridad en Java: mejores prácticas - 5Y también:Seguridad en Java: mejores prácticas - 6Esta es una gran herramienta para encontrar vulnerabilidades y monitorear actualizaciones para nuevas versiones.

Utilice el laboratorio de seguridad de GitHub

Cualquiera que trabaje en GitHub puede aprovechar sus herramientas integradas. Puede leer más sobre este enfoque en su publicación de blog titulada Anunciando el laboratorio de seguridad de GitHub . Esta herramienta, por supuesto, es más simple que Snyk, pero definitivamente no debes descuidarla. Además, la cantidad de vulnerabilidades conocidas solo crecerá, por lo que tanto Snyk como GitHub Security Lab continuarán expandiéndose y mejorando.

Activar Sonatype DepShield

Si usa GitHub para almacenar sus repositorios, puede agregar Sonatype DepShield, una de las aplicaciones en MarketPlace, a sus proyectos. También se puede usar para escanear proyectos en busca de dependencias. Además, si encuentra algo, se generará un problema de GitHub con una descripción adecuada, como se muestra a continuación:Seguridad en Java: mejores prácticas - 7

4. Maneje los datos confidenciales con cuidado

Alternativamente, podríamos usar la frase "datos confidenciales". La filtración de información personal, números de tarjetas de crédito y otra información confidencial de un cliente puede causar un daño irreparable. En primer lugar, eche un vistazo de cerca al diseño de su aplicación y determine si realmente necesita este o aquel dato. Tal vez en realidad no necesite algunos de los datos que tiene, datos que se agregaron para un futuro que no ha llegado y es poco probable que llegue. Además, muchos filtran inadvertidamente dichos datos a través del registro. Una manera fácil de evitar que los datos confidenciales ingresen a sus registros es borrar los métodos toString() de las entidades de dominio (como Usuario, Estudiante, Profesor, etc.). Esto evitará que generes accidentalmente campos confidenciales. Si usa Lombok para generar el toString()método, puede usar la anotación @ToString.Exclude para evitar que se use un campo en la salida del método toString() . Además, tenga mucho cuidado al enviar datos al mundo exterior. Supongamos que tenemos un punto final HTTP que muestra los nombres de todos los usuarios. No es necesario mostrar la identificación interna única de un usuario. ¿Por qué? Porque un atacante podría usarlo para obtener otra información más sensible sobre el usuario. Por ejemplo, si usa Jackson para serializar/deserializar un POJO a/desde JSON , entonces puede usar @JsonIgnore y @JsonIgnorePropertiesanotaciones para evitar la serialización/deserialización de campos específicos. En general, necesita usar diferentes clases de POJO en diferentes lugares. ¿Qué significa eso?
  1. Cuando trabaje con una base de datos, use un tipo de POJO (una entidad).
  2. Cuando trabaje con lógica empresarial, convierta una entidad en un modelo.
  3. Cuando trabaje con el mundo exterior y envíe solicitudes HTTP, use diferentes entidades (DTO).
De esta manera, puede definir claramente qué campos serán visibles desde el exterior y cuáles no.

Utilice algoritmos de cifrado y hash sólidos

Los datos confidenciales de los clientes deben almacenarse de forma segura. Para hacer esto, necesitamos usar encriptación. Dependiendo de la tarea, debe decidir qué tipo de cifrado usar. Además, un cifrado más fuerte lleva más tiempo, por lo que nuevamente debe considerar cuánto justifica la necesidad del tiempo dedicado a ello. Por supuesto, usted mismo puede escribir un algoritmo de cifrado. Pero esto es innecesario. Puede utilizar las soluciones existentes en esta área. Por ejemplo, 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>
Veamos qué hacer, usando este ejemplo que involucra cifrado y descifrado:

private static void encryptDecryptExample() {
   AeadConfig.register();
   KeysetHandle handle = KeysetHandle.generateNew(AeadKeyTemplates.AES128_CTR_HMAC_SHA256);

   String plaintext = "Elvis lives!";
   String aad = "Buddy Holly";

   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));
}

Cifrado de contraseñas

Para esta tarea, lo más seguro es utilizar el cifrado asimétrico. ¿Por qué? Porque la aplicación realmente no necesita descifrar contraseñas. Este es el enfoque estándar. En realidad, cuando un usuario ingresa una contraseña, el sistema la cifra y la compara con lo que existe en el almacén de contraseñas. Se realiza el mismo proceso de cifrado, por lo que podemos esperar que coincidan, si se ingresa la contraseña correcta, por supuesto :) BCrypt y SCrypt son adecuados aquí. Ambas son funciones unidireccionales (hashes criptográficos) con algoritmos computacionalmente complejos que toman mucho tiempo. Esto es exactamente lo que necesitamos, ya que los cálculos directos llevarán una eternidad (bueno, mucho, mucho tiempo). Spring Security admite una amplia gama de algoritmos. Podemos usar SCryptPasswordEncoder y BCryptPasswordEncoder. Lo que actualmente se considera un algoritmo de cifrado fuerte puede considerarse débil el próximo año. Como resultado, llegamos a la conclusión de que debemos comprobar periódicamente los algoritmos que utilizamos y, según sea necesario, actualizar las bibliotecas que contienen los algoritmos de cifrado.

En lugar de una conclusión

Hoy hablamos de seguridad y, naturalmente, muchas cosas se quedaron atrás. Acabo de abrir la puerta a un mundo nuevo para ti, un mundo que tiene vida propia. La seguridad es como la política: si no te ocupas de la política, la política se ocupará de ti. Tradicionalmente sugiero que me sigas en la cuenta de GitHub . Allí publico mis creaciones que involucran varias tecnologías que estoy estudiando y aplicando en el trabajo.

Enlaces útiles

  1. Guru99: Tutorial de Inyección SQL
  2. Oracle: Centro de recursos de seguridad de Java
  3. Oracle: Directrices de codificación segura para Java SE
  4. Baeldung: los fundamentos de la seguridad de Java
  5. Medio: 10 consejos para potenciar su seguridad Java
  6. Snyk: 10 mejores prácticas de seguridad de Java
  7. GitHub: Anunciamos GitHub Security Lab: asegurando el código del mundo, juntos
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION