Meilleures pratiques de sécurité Java
Failles de sécurité Java courantes et défenses : validation des entrées, désérialisation, dépendances, secrets.
La plupart des failles de sécurité Java ne sont pas exotiques. Il s'agit d'une vérification d'entrée manquante, d'une requête SQL construite par concaténation de chaînes, d'un mot de passe stocké sous forme de hachage simple, ou d'un secret commis dans Git. Ce chapitre présente les défenses qui stoppent la majorité des attaques réelles : valider tout ce qui franchit une frontière de confiance, ne jamais construire des requêtes par concaténation, hacher les mots de passe avec une fonction de dérivation de clé lente, garder les secrets hors du code et exécuter avec le minimum de privilèges nécessaires à la tâche.
Valider les entrées avec une liste d'autorisation
La première règle est de traiter toutes les entrées externes comme hostiles jusqu'à preuve du contraire : paramètres de requête, noms de fichiers, en-têtes, charges utiles de messages, tout ce qui franchit une frontière de confiance. Préférez une liste d'autorisation (accepter uniquement les formes connues et valides) à une liste de blocage (essayer de bloquer les mauvaises) — une liste de blocage rate toujours un cas.
// 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);Validez à la frontière du système et à nouveau à toute limite plus profonde que vous ne contrôlez pas. Rejetez tôt, échouez en mode fermé, et renvoyez une erreur générique pour ne pas divulguer la règle de validation à un attaquant sondant votre point de terminaison. Lorsque vous faites correspondre un motif, ancrez-le et gardez-le simple — consultez l'introduction aux expressions régulières pour voir comment matches vérifie la chaîne entière, pas seulement un fragment.
Utiliser des instructions préparées, jamais la concaténation de chaînes
L'injection SQL est toujours l'une des vulnérabilités web les plus courantes et les plus dommageables, et en Java elle est triviale à prévenir. Construisez des requêtes avec des paramètres liés via PreparedStatement ; le pilote envoie le modèle de requête et les valeurs séparément, de sorte que les données utilisateur ne peuvent jamais être analysées comme du 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 même idée s'applique au-delà de SQL : utilisez des API paramétrées pour LDAP, les commandes OS (ProcessBuilder avec une liste d'arguments, pas une chaîne shell), et tout modèle qui mélange code et données. Pour les détails JDBC, voir PreparedStatement et l'introduction JDBC.
Hacher les mots de passe avec une KDF lente
Les mots de passe ne doivent jamais être stockés en texte clair ni derrière un hachage rapide comme un seul tour de SHA-256 — les GPU modernes en testent des milliards par seconde. Utilisez une fonction de dérivation de clé délibérément lente et salée. Le JDK fournit PBKDF2 ; Argon2 et bcrypt sont d'excellentes options tierces.
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| Approche | Verdict |
|---|---|
| Texte clair / réversible | Jamais |
| MD5, SHA-1, SHA-256 simple | Trop rapide — cassé pour les mots de passe |
| PBKDF2 / bcrypt / Argon2 avec un sel par utilisateur | Correct |
| Même sel pour tous les utilisateurs | Annule l'utilité du sel |
Comparez toujours les hachages avec une vérification en temps constant (MessageDigest.isEqual) afin que le timing des réponses ne révèle pas quelle partie d'une supposition était correcte.
Garder les secrets hors du code
Les clés API, les mots de passe de base de données et les clés de signature n'ont pas leur place dans les fichiers source — une fois commis, ils vivent dans l'historique Git pour toujours. Lisez-les depuis l'environnement ou un gestionnaire de secrets au moment de l'exécution, et gardez les informations d'identification hors des journaux et des messages d'exception.
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.Utilisez SecureRandom (et non java.util.Random) pour tout ce qui est sensible à la sécurité — jetons, sels, nonces, identifiants de session. Random est prévisible et peut être initialisé, ce qui rend sa sortie devinable.
Appliquer le moindre privilège et des paramètres par défaut sûrs
Accordez à chaque composant uniquement l'accès dont il a besoin et rien de plus : un utilisateur de base de données en lecture seule pour les chemins de lecture, un compte de service limité à un seul compartiment, des permissions de fichier qui excluent le groupe et le monde. Validez les certificats TLS (ne désactivez jamais la vérification du nom d'hôte "pour que ça marche"), définissez des délais d'attente sur chaque appel réseau, et limitez la taille de tout ce que vous analysez pour éviter les dénis de service par des entrées ou des désérialisations de taille excessive.
// 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();Gardez les dépendances à jour — la plupart des violations exploitent une CVE connue dans une ancienne bibliothèque, donc exécutez un scanner (OWASP Dependency-Check, mvn versions:display-dependency-updates) dans la CI.
Le programme ci-dessous rassemble les idées essentielles : validation par liste d'autorisation, étirement de mot de passe salé, vérification en temps constant, et preuve que deux utilisateurs avec le même mot de passe obtiennent des hachages différents.
Ce qu'il faut retenir de l'exécution :
- La liste d'autorisation accepte
alice_99mais rejette à la foisRobert'); DROP TABLEet le trop courtab, de sorte que les entrées malveillantes ou malformées n'atteignent jamais la couche suivante. - L'étirement d'un mot de passe produit un condensé fixe de 32 octets sur 120 000 itérations — le coût est ce qui rend impraticable le forçage brutal du hachage stocké.
verifyretournetruepour le bon mot de passe etfalsepour le mauvais, car le hachage candidat ne correspond que lorsque l'entrée est identique.- Deux utilisateurs différents s'enregistrant avec le même mot de passe obtiennent des hachages inégaux (
same input, equal hash? false), prouvant que le sel aléatoire par utilisateur fait son travail. MessageDigest.isEqualrapportetruepour des octets identiques etfalsepour un changement d'un seul caractère, fournissant une comparaison en temps constant qui ne fuit pas par le timing.