Most Java vulnerabilities come from a small set of recurring mistakes, and each has a well-understood fix. This guide covers the ones that matter in ordinary application code: injection, unsafe deserialization, vulnerable dependencies, password storage, XML parsing, randomness and secrets handling.
1. SQL injection#
// Vulnerable
String sql = "SELECT * FROM users WHERE email = '" + email + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);
An input of ' OR '1'='1 returns every row. Prepared statements send the query and the data separately, so input cannot change the query’s structure:
String sql = "SELECT id, name FROM users WHERE email = ?";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, email);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getString("name"));
}
}
}
With JPA, the same rule applies — use named parameters, never string building.
2. Deserialization#
// Dangerous with untrusted input
ObjectInputStream in = new ObjectInputStream(request.getInputStream());
Object obj = in.readObject();
Java deserialization can construct arbitrary objects and invoke their methods during construction. With the right classes on the classpath, that becomes remote code execution — the mechanism behind a long series of high-profile vulnerabilities.
The rule is simple: never deserialize data you did not create. Use JSON with a well-configured parser instead:
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
// Do not enable default typing - that reintroduces the same class of problem
Order order = mapper.readValue(json, Order.class);
Where Java serialization is unavoidable, apply a filter:
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.model.*;java.base/*;!*");
in.setObjectInputFilter(filter);
3. Vulnerable dependencies#
In practice this is where most real risk sits. Your code may be fine; a library three levels down may not be.
mvn org.owasp:dependency-check-maven:check
gradle dependencyCheckAnalyze
Run it in your build pipeline so a new advisory fails the build rather than waiting to be noticed. Dependabot or Renovate will raise the update pull requests automatically.
4. Password storage#
// Never
String hash = DigestUtils.md5Hex(password);
String hash = DigestUtils.sha256Hex(password);
MD5 and SHA-256 are designed to be fast, which is exactly wrong for passwords — it makes brute-force cheap. Use an algorithm designed to be slow:
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
PasswordEncoder encoder = new BCryptPasswordEncoder(12);
String hash = encoder.encode(rawPassword); // includes its own salt
boolean ok = encoder.matches(rawPassword, hash);
Argon2 is the current recommendation where you can use it; bcrypt remains perfectly acceptable. Store the result in a column of at least 100 characters — a truncated hash fails verification for every user, and the cause is not obvious from the symptom.
5. XML external entities#
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature(
"http://xml.org/sax/features/external-general-entities", false);
factory.setFeature(
"http://xml.org/sax/features/external-parameter-entities", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
Without these, a crafted XML document can read files from your server or make it issue network requests. The defaults in older Java versions are unsafe, so set them explicitly on every parser you create.
6. Path traversal#
// Vulnerable
File file = new File("/var/data/" + request.getParameter("name"));
A name of ../../etc/passwd escapes the folder. Resolve and verify:
Path base = Paths.get("/var/data").toRealPath();
Path target = base.resolve(name).normalize();
if (!target.startsWith(base)) {
throw new SecurityException("Path outside the permitted directory");
}
normalize() collapses the dot segments; the startsWith check is what actually enforces the boundary.
7. Command injection#
// Vulnerable - a shell interprets the whole string
Runtime.getRuntime().exec("convert " + userInput + " out.png");
// Safer - arguments are passed separately, no shell involved
new ProcessBuilder("convert", userInput, "out.png").start();
Better still, avoid running external commands on user input at all. Where you must, validate the input against a strict allow-list first.
8. Randomness#
// Predictable - fine for shuffling, unsafe for tokens
Random random = new Random();
// Cryptographically secure
SecureRandom secure = new SecureRandom();
byte[] token = new byte[32];
secure.nextBytes(token);
String value = Base64.getUrlEncoder().withoutPadding().encodeToString(token);
Anything that grants access — session identifiers, password reset tokens, API keys — must come from SecureRandom.
9. Secrets#
// Never in source
private static final String API_KEY = "sk-1234567890";
// From the environment
String apiKey = System.getenv("API_KEY");
if (apiKey == null || apiKey.isBlank()) {
throw new IllegalStateException("API_KEY is not set");
}
Failing loudly at start-up is better than making a request with an empty key and getting a confusing 401 later. Once a secret is in git history, rotating it is the only real fix — removing it from the current file is not enough.
10. TLS#
Never install a trust manager that accepts every certificate. Disabling validation to make a development environment work removes the entire protection TLS provides, and that code invariably reaches production. Add the certificate to a trust store instead.
A checklist#
- Every query uses parameters, including in admin code
- No Java deserialization of untrusted input
- A dependency scan runs in the build
- Passwords use bcrypt or Argon2, column at least 100 characters
- XML parsers have external entities disabled
- File paths are resolved and checked against a base directory
- Tokens come from
SecureRandom - Secrets come from the environment or a secret manager
- The JDK and all dependencies are on supported versions
Questions people ask#
Is Java more secure than other languages?
It removes whole classes of memory-safety bugs that C and C++ have. It does not remove injection, deserialization or configuration mistakes, which is where application vulnerabilities actually come from.
Do I need the SecurityManager?
No — it is deprecated for removal. Use operating system and container isolation instead, which is where that responsibility now sits.
Should I use Spring Security?
For anything with authentication, yes. It provides password encoding, CSRF protection, session handling and security headers correctly by default. Writing that yourself means getting every detail right.
How often should I update dependencies?
Patch releases as they appear, ideally automatically. Minor and major versions on a regular schedule. A codebase that is years behind is far harder to secure than one kept current.
Where to go next#
- Java database connectivity — prepared statements in context.
- Java web development — where most of this applies.
- Java testing — including tests for the checks above.