W3docs

Best Practice di Sicurezza in Java

Errori di sicurezza comuni in Java e relative difese: validazione input, deserializzazione, dipendenze e segreti.

La maggior parte dei bug di sicurezza in Java non è esotica. Si tratta di un controllo di input mancante, una query SQL costruita tramite concatenazione di stringhe, una password memorizzata con un hash semplice o un segreto committato su Git. Questo capitolo illustra le difese che bloccano la maggior parte degli attacchi reali: validare tutto ciò che attraversa un confine di fiducia, non costruire mai query tramite concatenazione, eseguire l'hashing delle password con una funzione lenta di derivazione delle chiavi, tenere i segreti fuori dal codice ed eseguire con il minimo privilegio necessario per il compito.

Validare l'input con una allowlist

La prima regola è trattare tutto l'input esterno come ostile finché non si dimostra il contrario: parametri di richiesta, nomi di file, intestazioni, payload di messaggi, qualsiasi cosa attraversi un confine di fiducia. Preferire una allowlist (accettare solo forme note e valide) rispetto a una denylist (cercare di bloccare quelle cattive) — una denylist manca sempre qualcosa.

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

Validare al bordo del sistema e di nuovo a qualsiasi confine più profondo che non si controlla. Rifiutare subito, fallire in modo chiuso e restituire un errore generico per non rivelare la regola di validazione a un attaccante che sonda il proprio endpoint. Quando si fa un match contro un pattern, ancorarlo e mantenerlo semplice — vedere l'introduzione alle espressioni regolari per capire come matches controlla l'intera stringa, non solo un frammento.

Usare prepared statement, mai la concatenazione di stringhe

L'SQL injection è ancora una delle vulnerabilità web più comuni e più dannose, e in Java è banale da prevenire. Costruire le query con parametri bind attraverso PreparedStatement; il driver invia il template della query e i valori separatamente, quindi i dati utente non possono mai essere interpretati come 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 stessa idea si applica oltre SQL: usare API parametrizzate per LDAP, comandi OS (ProcessBuilder con una lista di argomenti, non una stringa shell) e qualsiasi template che mescola codice con dati. Per i dettagli JDBC, vedere PreparedStatement e l'introduzione a JDBC.

Eseguire l'hashing delle password con un KDF lento

Le password non devono mai essere memorizzate in testo semplice o con un hash veloce come un singolo round di SHA-256 — le GPU moderne ne provano miliardi al secondo. Usare una funzione di derivazione delle chiavi deliberatamente lenta e con salt. Il JDK include PBKDF2; Argon2 e bcrypt sono ottime opzioni di terze parti.

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
ApproccioVerdetto
Testo semplice / reversibileMai
MD5, SHA-1, singolo SHA-256Troppo veloce — inadatto per le password
PBKDF2 / bcrypt / Argon2 con salt per utenteCorretto
Stesso salt per ogni utenteVanifica lo scopo del salt

Confrontare sempre gli hash con un controllo a tempo costante (MessageDigest.isEqual) in modo che il timing delle risposte non riveli quanto fosse corretta una tentativo.

Tenere i segreti fuori dal codice

Le chiavi API, le password del database e le chiavi di firma non appartengono ai file sorgente — una volta committate vivono per sempre nella storia di Git. Leggerle dall'ambiente o da un gestore di segreti a runtime e tenere le credenziali fuori dai log e dai messaggi di eccezione.

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.

Usare SecureRandom (non java.util.Random) per qualsiasi cosa sensibile alla sicurezza — token, salt, nonce, ID di sessione. Random è prevedibile e con seed, il che rende il suo output indovinabile.

Applicare il minimo privilegio e i default sicuri

Dare a ogni componente solo l'accesso di cui ha bisogno e nient'altro: un utente database di sola lettura per i percorsi di lettura, un account di servizio limitato a un bucket, permessi sui file che escludono gruppo e altri. Validare i certificati TLS (non disabilitare mai la verifica dell'hostname "per farlo funzionare"), impostare timeout su ogni chiamata di rete e limitare la dimensione di qualsiasi cosa si analizzi per evitare attacchi denial-of-service tramite input di grandi dimensioni o deserializzazione.

// 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();

Mantenere le dipendenze aggiornate — la maggior parte delle violazioni sfrutta una CVE nota in una libreria vecchia, quindi eseguire uno scanner (OWASP Dependency-Check, mvn versions:display-dependency-updates) in CI.

Il programma seguente mette insieme le idee fondamentali: validazione con allowlist, stretching della password con salt, verifica a tempo costante e dimostrazione che due utenti con la stessa password ottengono hash diversi.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • La allowlist accetta alice_99 ma rifiuta sia Robert'); DROP TABLE che il troppo corto ab, quindi input malevoli o malformati non raggiungono mai il livello successivo.
  • Lo stretching di una password produce un digest fisso di 32 byte in 120.000 iterazioni — il costo è ciò che rende impraticabile il brute-forcing dell'hash memorizzato.
  • verify restituisce true per la password corretta e false per quella sbagliata, perché l'hash candidato corrisponde solo quando l'input è identico.
  • Due utenti diversi che registrano la stessa password ottengono hash disuguali (same input, equal hash? false), dimostrando che il salt casuale per utente fa il suo lavoro.
  • MessageDigest.isEqual riporta true per byte identici e false per un cambio di un carattere, fornendo un confronto a tempo costante che non perde informazioni tramite il timing.

Esercitazione

Pratica
Perché le password devono essere memorizzate con una funzione lenta di derivazione delle chiavi con salt come PBKDF2 invece di un singolo round di SHA-256?
Perché le password devono essere memorizzate con una funzione lenta di derivazione delle chiavi con salt come PBKDF2 invece di un singolo round di SHA-256?
Was this page helpful?