Java Security Best Practices
Common Java security pitfalls and defenses — input validation, deserialization, dependencies, secrets.
Java Security Best Practices
Most Java security bugs are not exotic. They are a missing input check, a string-concatenated SQL query, a password stored as a plain hash, or a secret committed to Git. This chapter walks through the defenses that stop the majority of real-world attacks: validate everything that crosses a trust boundary, never build queries by concatenation, hash passwords with a slow key-derivation function, keep secrets out of code, and run with the least privilege the task needs.
Validate input on an allowlist
The first rule is to treat all external input as hostile until proven otherwise: request parameters, file names, headers, message payloads, anything that crosses a trust boundary. Prefer an allowlist (accept only known-good shapes) over a denylist (try to block bad ones) — a denylist always misses a case.
// 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);Validate at the edge of the system and again at any deeper boundary you do not control. Reject early, fail closed, and return a generic error so you do not leak the validation rule to an attacker probing your endpoint.
Use prepared statements, never string concatenation
SQL injection is still one of the most common and most damaging web vulnerabilities, and in Java it is trivial to prevent. Build queries with bind parameters through PreparedStatement; the driver sends the query template and the values separately, so user data can never be parsed as 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"));
}
}The same idea applies beyond SQL: use parameterized APIs for LDAP, OS commands (ProcessBuilder with an argument list, not a shell string), and any template that mixes code with data.
Hash passwords with a slow KDF
Passwords must never be stored in plain text or behind a fast hash like a single round of SHA-256 — modern GPUs try billions of those per second. Use a deliberately slow, salted key-derivation function. The JDK ships PBKDF2; Argon2 and bcrypt are excellent third-party options.
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| Approach | Verdict |
|---|---|
| Plain text / reversible | Never |
| MD5, SHA-1, single SHA-256 | Far too fast — broken for passwords |
| PBKDF2 / bcrypt / Argon2 with a per-user salt | Correct |
| Same salt for every user | Defeats the salt's purpose |
Always compare hashes with a constant-time check (MessageDigest.isEqual) so response timing does not reveal how much of a guess was right.
Keep secrets out of code
API keys, database passwords, and signing keys do not belong in source files — once committed they live in Git history forever. Read them from the environment or a secrets manager at runtime, and keep credentials out of logs and exception messages.
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.Use SecureRandom (not java.util.Random) for anything security-sensitive — tokens, salts, nonces, session IDs. Random is predictable and seedable, which makes its output guessable.
Apply least privilege and safe defaults
Give every component only the access it needs and nothing more: a read-only database user for read paths, a service account scoped to one bucket, file permissions that exclude group and world. Validate TLS certificates (never disable hostname verification "to make it work"), set timeouts on every network call, and cap the size of anything you parse to avoid denial-of-service through oversized input or deserialization.
// 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();Keep dependencies patched — most breaches exploit a known CVE in an old library, so run a scanner (OWASP Dependency-Check, mvn versions:display-dependency-updates) in CI.
The program below puts the core ideas together: allowlist validation, salted password stretching, constant-time verification, and proof that two users with the same password get different hashes.
What to take from the run:
- The allowlist accepts
alice_99but rejects bothRobert'); DROP TABLEand the too-shortab, so malicious or malformed input never reaches the next layer. - Stretching a password produces a fixed 32-byte digest over 120,000 iterations — the cost is what makes brute-forcing the stored hash impractical.
verifyreturnstruefor the correct password andfalsefor the wrong one, because the candidate hash only matches when the input is identical.- Two different users registering the very same password get unequal hashes (
same input, equal hash? false), proving the per-user random salt does its job. MessageDigest.isEqualreportstruefor identical bytes andfalsefor a one-character change, giving a constant-time comparison that does not leak through timing.
Practice
Why must passwords be stored with a slow, salted key-derivation function such as PBKDF2 instead of a single round of SHA-256?