JavaRush /Blog Java /Random-ES /Seguridad en Java: mejores prácticas

Seguridad en Java: mejores prácticas

Publicado en el grupo Random-ES
En las aplicaciones de servidor, uno de los indicadores más importantes es la seguridad. Este es uno de los tipos de Requisitos No Funcionales . Seguridad en Java: mejores prácticas - 1La seguridad tiene muchos componentes. Por supuesto, para poder abarcar por completo todos esos principios y acciones protectoras que se conocen es necesario escribir más de un artículo, así que centrémonos en los más importantes. Cualquier equipo necesitará una persona que conozca bien este tema y que sea capaz de configurar todos los procesos y asegurarse de que no creen nuevos agujeros de seguridad. Eso sí, no debes pensar que si sigues estas prácticas la aplicación será completamente segura. ¡No! Pero definitivamente será más seguro con ellos. Ir.

1. Proporcionar seguridad a nivel del lenguaje Java.

En primer lugar, la seguridad en Java comienza justo en el nivel de las características del lenguaje. ¿Esto es lo que haríamos si no existieran modificadores de acceso?... Anarquía, nada menos. Un lenguaje de programación nos ayuda a escribir código seguro y también a aprovechar muchas características de seguridad implícitas:
  1. Escritura fuerte. Java es un lenguaje de tipo estático que proporciona la capacidad de detectar errores de tipo en tiempo de ejecución.
  2. Modificadores de acceso. Gracias a ellos podemos configurar el acceso a clases, métodos y campos de clase como necesitemos.
  3. Gestión automática de la memoria. Para ello, nosotros (javaístas ;)) contamos con Garbage Collector, que lo libera de la configuración manual. Sí, a veces surgen problemas.
  4. Comprobación de código de bytes: Java se compila en código de bytes, que el tiempo de ejecución verifica antes de ejecutarlo.
Entre otras cosas, existen recomendaciones de Oracle sobre seguridad. Por supuesto, no está escrito con un “estilo sofisticado” y es posible que te quedes dormido varias veces mientras lo lees, pero vale la pena. Un documento particularmente importante son las Pautas de codificación segura para Java SE , que brindan consejos sobre cómo escribir código seguro. Este documento contiene mucha información útil. Si es posible, definitivamente vale la pena leerlo. Para despertar el interés por este material, aquí tienes algunos consejos interesantes:
  1. Evite serializar clases sensibles a la seguridad. En este caso, puede obtener la interfaz de clase del archivo serializado, sin mencionar los datos que se están serializando.
  2. Intente evitar las clases de datos mutables. Esto brinda todos los beneficios de las clases inmutables (por ejemplo, seguridad de subprocesos). Si hay un objeto mutable, esto 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, entonces el código del cliente puede cambiar el estado interno del objeto.
  4. Etcétera…
En general, las Pautas de codificación segura para Java SE contienen un conjunto de consejos y trucos sobre cómo escribir código en Java de forma correcta y segura.

2. Eliminar la vulnerabilidad de inyección SQL

Vulnerabilidad única. Su singularidad radica en el hecho de que es a la vez una de las vulnerabilidades más famosas y una de las más comunes. Si no está interesado en el tema de la seguridad, entonces no lo sabrá. ¿Qué es la inyección SQL? Este es un ataque a una base de datos mediante la inyección de código SQL adicional donde no se espera. Digamos que tenemos un método que toma algún parámetro para consultar la base de datos. Por ejemplo, nombre de usuario. El código con la vulnerabilidad se verá así:
// Метод достает из базы данных всех пользователей с определенным именем
public List<User> findByFirstName(String firstName) throws SQLException {
   // Создается связь с базой данных
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);

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

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

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

private List<User> mapToUsers(ResultSet resultSet) {
   //переводит в коллекцию юзеров
}
En este ejemplo, la consulta SQL se prepara de antemano en una línea separada. Parecería que cuál es el problema, ¿verdad? ¿Quizás el problema es que sería mejor usarlo String.format? ¿No? ¿Entonces que? Pongámonos en el lugar de un evaluador y pensemos en lo que se puede transmitir en el valor firstName. Por ejemplo:
  1. Puede pasar lo que se espera: el nombre de usuario. Luego la base de datos devolverá todos los usuarios con ese nombre.
  2. Puede pasar una cadena vacía: entonces se devolverán todos los usuarios.
  3. O puede pasar lo siguiente: “''; USUARIOS DE LA MESA DE GOTA;”. Y aquí habrá problemas mayores. Esta consulta eliminará la tabla de la base de datos. Con todos los datos. TODOS.
¿Te imaginas los problemas que esto podría causar? Entonces puedes escribir lo que quieras. Puede cambiar el nombre de todos los usuarios, puede eliminar sus direcciones. El margen para el sabotaje es enorme. Para evitar esto, debe dejar de inyectar una consulta ya preparada y, en su lugar, crearla utilizando parámetros. Esta debería ser la única forma de consultar la base de datos. De esta forma podrás eliminar esta vulnerabilidad. Ejemplo:
// Метод достает из базы данных всех пользователей с определенным именем
public List<User> findByFirstName(String firstName) throws SQLException {
   // Создается связь с базой данных
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);

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

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

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

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

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

private List<User> mapToUsers(ResultSet resultSet) {
   //переводим в коллекцию юзеров
}
De esta forma se evita esta vulnerabilidad. Para aquellos que quieran profundizar en este artículo, aquí hay un gran ejemplo . ¿Cómo sabes si entiendes esta parte? Si el chiste siguiente quedó claro, entonces es una señal segura de que la esencia de la vulnerabilidad es clara :D Seguridad en Java: mejores prácticas - 2

3. Escanee y mantenga las dependencias actualizadas

¿Qué significa? Para aquellos que no saben qué es una dependencia, les explicaré: es un archivo jar con código que se conecta a un proyecto mediante sistemas de compilación automática (Maven, Gradle, Ant) para poder reutilizar la solución de otra persona. Por ejemplo, Project Lombok , que genera captadores, definidores, etc. para nosotros en tiempo de ejecución. Y si hablamos de aplicaciones grandes, utilizan muchas dependencias diferentes. Algunas son transitivas (es decir, cada dependencia puede tener sus propias dependencias, etc.). Por lo tanto, los atacantes prestan cada vez más atención a las dependencias de código abierto, ya que se utilizan con regularidad y pueden causar problemas a muchos clientes. Es importante asegurarse de que no haya vulnerabilidades conocidas en todo el árbol de dependencias (que es exactamente lo que parece). Y hay varias maneras de hacer esto.

Utilice Snyk para monitorear

La herramienta Snyk verifica todas las dependencias del proyecto y señala las vulnerabilidades conocidas. Allí podrá registrar e importar sus proyectos a través de GitHub, por ejemplo. Seguridad en Java: mejores prácticas - 3Además, como puede ver en la imagen de arriba, si una versión más nueva tiene una solución para esta vulnerabilidad, Snyk ofrecerá hacerlo y crear una solicitud de extracción. Se puede utilizar de forma gratuita para proyectos de código abierto. Los proyectos se escanearán con cierta frecuencia: 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 están abiertos a todos). A continuación, Snyk mostró el resultado del análisis: Seguridad en Java: mejores prácticas - 4Y después de un tiempo, Snyk-bot preparó varias solicitudes de extracción en proyectos donde es necesario actualizar las dependencias: Seguridad en Java: mejores prácticas - 5Y aquí hay otra: Seguridad en Java: mejores prácticas - 6Esta es una excelente herramienta para buscar vulnerabilidades y monitorear actualizaciones. nuevas versiones.

Utilice el laboratorio de seguridad de GitHub

Quienes trabajan en GitHub también pueden aprovechar sus herramientas integradas. Puede leer más sobre este enfoque en mi traducción de su blog Anuncio del laboratorio de seguridad de GitHub . Esta herramienta, por supuesto, es más sencilla que Snyk, pero definitivamente no debes descuidarla. Además, la cantidad de vulnerabilidades conocidas no hará más que crecer, por lo que tanto Snyk como GitHub Security Lab se expandirán y mejorarán.

Activar Sonatype DepShield

Si usa GitHub para almacenar sus repositorios, puede agregar una de las aplicaciones a sus proyectos desde MarketPlace: Sonatype DepShield. Con su ayuda, también puedes escanear proyectos en busca de dependencias. Además, si encuentra algo, se creará un problema de GitHub con la descripción correspondiente, como se muestra a continuación: Seguridad en Java: mejores prácticas - 7

4. Maneje los datos confidenciales con cuidado

Seguridad en Java: mejores prácticas - 8En el habla inglesa, la frase "datos sensibles" es más común. La divulgación de información personal, números de tarjetas de crédito y otra información personal del cliente puede causar un daño irreparable. En primer lugar, debe observar de cerca el diseño de la aplicación y determinar si realmente se necesitan datos. Quizás algunos de ellos no sean necesarios, pero se agregaron para un futuro que no ha llegado y es poco probable que llegue. Además, durante el registro del proyecto, dichos datos pueden filtrarse. Una forma sencilla de evitar que datos confidenciales entren en sus registros es limpiar los métodos toString()de las entidades del dominio (como Usuario, Estudiante, Profesor, etc.). Esto evitará que los campos confidenciales se impriman accidentalmente. Si usa Lombok para generar un método toString(), puede usar una anotación @ToString.Excludepara evitar que el campo se use en la salida a través del método toString(). Además, tenga mucho cuidado al compartir datos con el mundo exterior. Por ejemplo, hay un punto final http que muestra los nombres de todos los usuarios. No es necesario mostrar el ID único interno del usuario. ¿Por qué? Porque al usarlo, un atacante puede obtener otra información más confidencial sobre cada usuario. Por ejemplo, si usa Jackson para serializar y deserializar POJO en JSON , puede usar las anotaciones @JsonIgnorey @JsonIgnorePropertiespara evitar que campos específicos se serialicen o deserialicen. En general, es necesario utilizar diferentes clases de POJO para diferentes lugares. ¿Qué significa?
  1. Para trabajar con la base de datos, use solo POJO - Entidad.
  2. Para trabajar con lógica empresarial, transfiera Entidad a Modelo.
  3. Para trabajar con el mundo exterior y enviar solicitudes http, utilice entidades de terceros: DTO.
De esta forma podrás definir claramente qué campos serán visibles desde el exterior y cuáles no.

Utilice algoritmos de hash y cifrado sólidos

Los datos confidenciales de los clientes deben almacenarse de forma segura. Para hacer esto necesitas usar cifrado. Dependiendo de la tarea, deberá decidir qué tipo de cifrado utilizar. Además, un cifrado más seguro requiere más tiempo, por lo que nuevamente es necesario considerar en qué medida su necesidad justifica el tiempo dedicado a ello. Por supuesto, puedes escribir el algoritmo tú mismo. Pero esto es innecesario. Puede aprovechar 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 cómo usarlo, usando el ejemplo de cómo cifrar de una forma y de otra:
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));
}

Cifrado de contraseña

Para esta tarea, lo más seguro es utilizar cifrado asimétrico. ¿Por qué? Porque la aplicación realmente no necesita descifrar las contraseñas. Este es el enfoque general. En realidad, cuando un usuario ingresa una contraseña, el sistema la cifra y la compara con lo que hay en la bóveda de contraseñas. El cifrado se lleva a cabo utilizando los mismos medios, por lo que puede esperar que coincidan (si ingresa la contraseña correcta;), por supuesto). BCrypt y SCrypt son adecuados para este propósito. Ambas son funciones unidireccionales (hashes criptográficos) con algoritmos computacionalmente complejos que requieren mucho tiempo. Esto es exactamente lo que necesitas, ya que descifrarlo de frente te llevará una eternidad. Por ejemplo, Spring Security admite una variedad de algoritmos. SCryptPasswordEncoderTambién puedes usar BCryptPasswordEncoder. Lo que ahora es un algoritmo de cifrado sólido puede resultar débil el próximo año. Como resultado, concluimos que es necesario verificar los algoritmos utilizados y actualizar las bibliotecas con algoritmos.

En lugar de salida

Hoy hablamos de seguridad y, por supuesto, muchas cosas quedaron detrás de escena. Acabo de abrirte la puerta a un mundo nuevo: un mundo que vive su propia vida. Con la seguridad ocurre lo mismo que con la política: si no haces política, la política se ocupará de ti. Tradicionalmente, sugiero suscribirme a mi cuenta de Github . Allí publico mis trabajos sobre diversas tecnologías que estudio y aplico en el trabajo.

Enlaces útiles

Sí, casi todos los artículos del sitio están escritos en inglés. Nos guste o no, el inglés es el idioma para que los programadores se comuniquen. Todos los artículos, libros y revistas más recientes sobre programación están escritos en inglés. Por eso mis enlaces a recomendaciones están en su mayoría en inglés:
  1. Habr: Inyección SQL para principiantes
  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 la seguridad de Java
  6. Snyk: 10 mejores prácticas de seguridad de Java
  7. JR: Anuncio de GitHub Security Lab: protegiendo todo tu código en conjunto
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION