W3docs

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
ApprocheVerdict
Texte clair / réversibleJamais
MD5, SHA-1, SHA-256 simpleTrop rapide — cassé pour les mots de passe
PBKDF2 / bcrypt / Argon2 avec un sel par utilisateurCorrect
Même sel pour tous les utilisateursAnnule 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.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • La liste d'autorisation accepte alice_99 mais rejette à la fois Robert'); DROP TABLE et le trop court ab, 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é.
  • verify retourne true pour le bon mot de passe et false pour 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.isEqual rapporte true pour des octets identiques et false pour un changement d'un seul caractère, fournissant une comparaison en temps constant qui ne fuit pas par le timing.

Pratique

Pratique
Pourquoi les mots de passe doivent-ils être stockés avec une fonction de dérivation de clé lente et salée telle que PBKDF2 plutôt qu'avec un seul tour de SHA-256 ?
Pourquoi les mots de passe doivent-ils être stockés avec une fonction de dérivation de clé lente et salée telle que PBKDF2 plutôt qu'avec un seul tour de SHA-256 ?
Was this page helpful?