W3docs

Mejores Prácticas de Seguridad en Java

Errores de seguridad comunes en Java y sus defensas: validación de entrada, deserialización, dependencias y secretos.

La mayoría de los errores de seguridad en Java no son exóticos. Son una verificación de entrada omitida, una consulta SQL construida con concatenación de cadenas, una contraseña almacenada como un hash simple, o un secreto confirmado en Git. Este capítulo recorre las defensas que detienen la mayoría de los ataques del mundo real: valida todo lo que cruza un límite de confianza, nunca construyas consultas por concatenación, hashea contraseñas con una función de derivación de claves lenta, mantén los secretos fuera del código y ejecuta con el mínimo privilegio que la tarea necesita.

Valida la entrada con una lista de permitidos

La primera regla es tratar toda entrada externa como hostil hasta que se demuestre lo contrario: parámetros de solicitud, nombres de archivos, cabeceras, cargas de mensajes, cualquier cosa que cruce un límite de confianza. Prefiere una lista de permitidos (acepta solo formas conocidas y válidas) sobre una lista de bloqueados (intenta bloquear las malas) — una lista de bloqueados siempre omite un caso.

// Allowlist: only lowercase letters, digits and underscore, 3–16 chars.
static boolean isValidUsername(String s) {
    return s != null && s.matches("[a-z0-9_]{3,16}");
}

// Constrain numbers to a sane range instead of trusting the caller.
int page = Math.clamp(requested, 1, 1000);

Valida en el borde del sistema y de nuevo en cualquier límite más profundo que no controles. Rechaza temprano, falla de forma cerrada y devuelve un error genérico para no filtrar la regla de validación a un atacante que sondea tu endpoint. Cuando hagas coincidir con un patrón, anclalo y mantenlo simple — consulta la introducción a las expresiones regulares para ver cómo matches verifica la cadena completa, no solo un fragmento.

Usa sentencias preparadas, nunca concatenación de cadenas

La inyección SQL sigue siendo una de las vulnerabilidades web más comunes y dañinas, y en Java es trivial de prevenir. Construye consultas con parámetros de enlace a través de PreparedStatement; el controlador envía la plantilla de consulta y los valores por separado, por lo que los datos del usuario nunca pueden interpretarse como SQL.

// NEVER do this — user input becomes part of the query text.
String bad = "SELECT * FROM users WHERE name = '" + name + "'";

// Do this — the value is bound, not concatenated.
String sql = "SELECT id FROM users WHERE name = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setString(1, name);
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) process(rs.getLong("id"));
    }
}

La misma idea se aplica más allá de SQL: usa API parametrizadas para LDAP, comandos del sistema operativo (ProcessBuilder con una lista de argumentos, no una cadena de shell) y cualquier plantilla que mezcle código con datos. Para los detalles de JDBC, consulta PreparedStatement y la introducción a JDBC.

Hashea contraseñas con una KDF lenta

Las contraseñas nunca deben almacenarse en texto plano ni detrás de un hash rápido como una sola ronda de SHA-256 — las GPU modernas prueban miles de millones de ellas por segundo. Usa una función de derivación de claves deliberadamente lenta y con sal. El JDK incluye PBKDF2; Argon2 y bcrypt son excelentes opciones de terceros.

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;

byte[] salt = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(salt);        // unique per user

var spec = new PBEKeySpec(password, salt, 600_000, 256);  // iterations, key bits
var skf  = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] hash = skf.generateSecret(spec).getEncoded();
spec.clearPassword();                                     // wipe the secret
EnfoqueVeredicto
Texto plano / reversibleNunca
MD5, SHA-1, SHA-256 simpleDemasiado rápido — roto para contraseñas
PBKDF2 / bcrypt / Argon2 con sal por usuarioCorrecto
Misma sal para todos los usuariosAnula el propósito de la sal

Siempre compara hashes con una verificación en tiempo constante (MessageDigest.isEqual) para que el tiempo de respuesta no revele cuánto de una suposición era correcta.

Mantén los secretos fuera del código

Las claves API, las contraseñas de base de datos y las claves de firma no pertenecen a los archivos fuente — una vez confirmados, viven en el historial de Git para siempre. Léelos del entorno o de un gestor de secretos en tiempo de ejecución, y mantén las credenciales fuera de los registros y los mensajes de excepción.

String dbPassword = System.getenv("DB_PASSWORD");
if (dbPassword == null || dbPassword.isBlank()) {
    throw new IllegalStateException("DB_PASSWORD is not configured");
}
// Hold short-lived secrets in char[]/byte[] and wipe them, not String,
// because String is immutable and lingers in the heap until GC.

Usa SecureRandom (no java.util.Random) para cualquier cosa sensible a la seguridad — tokens, sales, nonces, IDs de sesión. Random es predecible y con semilla, lo que hace que su salida sea adivinable.

Aplica el mínimo privilegio y los valores predeterminados seguros

Dale a cada componente solo el acceso que necesita y nada más: un usuario de base de datos de solo lectura para rutas de lectura, una cuenta de servicio con alcance a un solo bucket, permisos de archivo que excluyan grupo y mundo. Valida los certificados TLS (nunca deshabilites la verificación de nombre de host "para que funcione"), establece tiempos de espera en cada llamada de red y limita el tamaño de todo lo que analices para evitar la denegación de servicio mediante entrada excesivamente grande o deserialización.

// Never deserialize untrusted bytes with Java's native serialization.
// Prefer a data format you can validate (JSON/Protobuf) and bound its size.
HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(5))   // fail fast, don't hang
    .build();

Mantén las dependencias parcheadas — la mayoría de las brechas explotan un CVE conocido en una biblioteca antigua, así que ejecuta un escáner (OWASP Dependency-Check, mvn versions:display-dependency-updates) en CI.

El programa a continuación reúne las ideas principales: validación con lista de permitidos, estiramiento de contraseñas con sal, verificación en tiempo constante y prueba de que dos usuarios con la misma contraseña obtienen hashes diferentes.

java— editable, runs on the server

Lo que se puede extraer de la ejecución:

  • La lista de permitidos acepta alice_99 pero rechaza tanto Robert'); DROP TABLE como el demasiado corto ab, por lo que la entrada maliciosa o malformada nunca llega a la siguiente capa.
  • Estirar una contraseña produce un resumen fijo de 32 bytes en 120,000 iteraciones — el costo es lo que hace que la fuerza bruta del hash almacenado sea impráctica.
  • verify devuelve true para la contraseña correcta y false para la incorrecta, porque el hash candidato solo coincide cuando la entrada es idéntica.
  • Dos usuarios diferentes que registran exactamente la misma contraseña obtienen hashes desiguales (same input, equal hash? false), lo que demuestra que la sal aleatoria por usuario cumple su función.
  • MessageDigest.isEqual informa true para bytes idénticos y false para un cambio de un carácter, proporcionando una comparación en tiempo constante que no se filtra a través del tiempo de respuesta.

Práctica

Práctica
¿Por qué las contraseñas deben almacenarse con una función de derivación de claves lenta y con sal como PBKDF2 en lugar de una sola ronda de SHA-256?
¿Por qué las contraseñas deben almacenarse con una función de derivación de claves lenta y con sal como PBKDF2 en lugar de una sola ronda de SHA-256?
Was this page helpful?