Java-Sicherheits-Best-Practices
Häufige Java-Sicherheitsfallen und Gegenmaßnahmen: Eingabevalidierung, Deserialisierung, Abhängigkeiten und Geheimnisse.
Die meisten Java-Sicherheitsfehler sind nicht exotisch. Es handelt sich um eine fehlende Eingabeprüfung, eine per String-Konkatenation aufgebaute SQL-Abfrage, ein als einfachem Hash gespeichertes Passwort oder ein in Git committetes Geheimnis. Dieses Kapitel beschreibt die Abwehrmaßnahmen, die den Großteil realer Angriffe verhindern: alles validieren, was eine Vertrauensgrenze überschreitet, niemals Abfragen durch Konkatenation aufbauen, Passwörter mit einer langsamen Schlüsselableitungsfunktion hashen, Geheimnisse aus dem Code heraushalten und mit den geringstmöglichen Rechten arbeiten, die die Aufgabe erfordert.
Eingaben mit einer Positivliste validieren
Die erste Regel lautet, alle externen Eingaben als feindlich zu betrachten, bis das Gegenteil bewiesen ist: Anfrageparameter, Dateinamen, Header, Nachrichten-Payloads – alles, was eine Vertrauensgrenze überschreitet. Bevorzuge eine Positivliste (akzeptiere nur bekannte gültige Formen) gegenüber einer Negativliste (versuche, schlechte Eingaben zu blockieren) – eine Negativliste übersieht immer einen Fall.
// 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);Validiere am Rand des Systems und erneut an jeder tieferen Grenze, die du nicht kontrollierst. Lehne früh ab, scheitere geschlossen und gib einen generischen Fehler zurück, damit du die Validierungsregel nicht an einen Angreifer verrätst, der deinen Endpunkt auslotet. Wenn du gegen ein Muster abgleichst, verankere es und halte es einfach – sieh dir die Einführung in reguläre Ausdrücke an, um zu verstehen, wie matches den gesamten String prüft und nicht nur ein Fragment.
Prepared Statements verwenden, niemals String-Konkatenation
SQL-Injection ist nach wie vor eine der häufigsten und schädlichsten Web-Sicherheitslücken, und in Java ist sie trivial zu verhindern. Baue Abfragen mit Bind-Parametern über PreparedStatement auf; der Treiber sendet die Abfragevorlage und die Werte getrennt, sodass Benutzerdaten niemals als SQL interpretiert werden können.
// 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"));
}
}Dasselbe Prinzip gilt über SQL hinaus: Verwende parametrisierte APIs für LDAP, OS-Befehle (ProcessBuilder mit einer Argumentliste, nicht einem Shell-String) und jede Vorlage, die Code mit Daten vermischt. Für die JDBC-Details, sieh dir PreparedStatement und die JDBC-Einführung an.
Passwörter mit einer langsamen KDF hashen
Passwörter dürfen niemals im Klartext oder hinter einem schnellen Hash wie einer einzigen SHA-256-Runde gespeichert werden – moderne GPUs verarbeiten Milliarden davon pro Sekunde. Verwende eine absichtlich langsame, gesaltete Schlüsselableitungsfunktion. Das JDK enthält PBKDF2; Argon2 und bcrypt sind ausgezeichnete Drittanbieteroptionen.
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| Ansatz | Bewertung |
|---|---|
| Klartext / umkehrbar | Niemals |
| MD5, SHA-1, einfaches SHA-256 | Viel zu schnell – für Passwörter ungeeignet |
| PBKDF2 / bcrypt / Argon2 mit einem benutzerspezifischen Salt | Korrekt |
| Gleicher Salt für jeden Benutzer | Macht den Zweck des Salts zunichte |
Vergleiche Hashes immer mit einer Konstantzeit-Prüfung (MessageDigest.isEqual), damit das Antwort-Timing nicht verrät, wie viel eines Rateversuchs korrekt war.
Geheimnisse aus dem Code heraushalten
API-Schlüssel, Datenbankpasswörter und Signing-Keys gehören nicht in Quelldateien – einmal committed, leben sie für immer in der Git-Historie. Lese sie zur Laufzeit aus der Umgebung oder einem Secrets-Manager und halte Zugangsdaten aus Logs und Exception-Meldungen heraus.
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.Verwende SecureRandom (nicht java.util.Random) für alles Sicherheitsrelevante – Tokens, Salts, Nonces, Session-IDs. Random ist vorhersagbar und seedbar, was seine Ausgabe erratbar macht.
Minimale Rechte und sichere Standardwerte anwenden
Gib jeder Komponente nur den Zugriff, den sie benötigt, und nichts mehr: einen Nur-Lese-Datenbankbenutzer für Lesepfade, ein Service-Konto, das auf einen Bucket beschränkt ist, Dateiberechtigungen, die Gruppe und Welt ausschließen. Validiere TLS-Zertifikate (deaktiviere niemals die Hostname-Verifizierung „damit es funktioniert"), setze Timeouts bei jedem Netzwerkaufruf und begrenze die Größe von allem, was du parsest, um Denial-of-Service durch überdimensionierte Eingaben oder Deserialisierung zu verhindern.
// 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();Halte Abhängigkeiten gepatcht – die meisten Sicherheitsverletzungen nutzen eine bekannte CVE in einer alten Bibliothek aus, also führe einen Scanner (OWASP Dependency-Check, mvn versions:display-dependency-updates) in CI aus.
Das folgende Programm bringt die Kernideen zusammen: Positivlisten-Validierung, gesaltetes Passwort-Stretching, Konstantzeit-Verifizierung und den Nachweis, dass zwei Benutzer mit demselben Passwort unterschiedliche Hashes erhalten.
Was aus der Ausführung zu entnehmen ist:
- Die Positivliste akzeptiert
alice_99, lehnt aber sowohlRobert'); DROP TABLEals auch das zu kurzeabab, sodass bösartige oder fehlerhafte Eingaben nie die nächste Schicht erreichen. - Das Strecken eines Passworts erzeugt über 120.000 Iterationen einen festen 32-Byte-Digest – die Kosten sind es, die das Brute-Forcing des gespeicherten Hashes unpraktikabel machen.
verifygibttruefür das richtige Passwort undfalsefür das falsche zurück, da der Kandidaten-Hash nur übereinstimmt, wenn die Eingabe identisch ist.- Zwei verschiedene Benutzer, die dasselbe Passwort registrieren, erhalten ungleiche Hashes (
same input, equal hash? false), was beweist, dass der benutzerspezifische Zufalls-Salt seinen Zweck erfüllt. MessageDigest.isEqualmeldettruefür identische Bytes undfalsefür eine einzelne Zeichenänderung und liefert einen Konstantzeit-Vergleich, der nicht durch Timing verraten wird.