Most Java security advice arrives as an undifferentiated list of thirty things you should "always do," which is exactly why so little of it gets done. Engineers reading a flat checklist can't tell which item stops a breach and which is a nicety. The uncomfortable truth is that the two most damaging Java incidents of the last decade — the 2017 Equifax breach via an unpatched Apache Struts2 flaw (CVE-2017-5638) and Log4Shell (CVE-2021-44228, CVSS 10.0) — were not clever exploits of application logic. They were failures of dependency hygiene and unsafe default behavior. This guide organizes Java security around the phases where decisions actually get made: how you handle input, how you use cryptography, how you manage dependencies, and how you run the JVM in production.
What actually causes most Java security incidents?
The dominant cause is vulnerable third-party code, not bugs in your own classes. A modern Spring or Jakarta EE application routinely pulls in over a hundred transitive dependencies, and OWASP's Top 10 (2021) ranks "Vulnerable and Outdated Components" as category A06 precisely because this is where real-world compromise concentrates. So the single highest-leverage practice is knowing — continuously — what is on your classpath and whether any of it is vulnerable. Everything else in this guide matters, but it matters second.
Handle input as hostile by default
Injection flaws survive into 2026 because Java's defaults are permissive. String-concatenated SQL compiles cleanly, and standard XML parsers resolve external entities unless you disable them.
For database access, bind every parameter. Never build a query with string concatenation:
// Vulnerable: user input becomes part of the SQL text
String sql = "SELECT * FROM users WHERE email = '" + email + "'";
// Safe: parameter is bound, never parsed as SQL
String sql = "SELECT * FROM users WHERE email = ?";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, email);
ResultSet rs = ps.executeQuery();
}
For XML, disable external entities to prevent XXE. Do it once in a shared factory so nobody re-instantiates an unsafe parser:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
Validate input against an allow-list of expected shapes rather than trying to block-list "bad" characters — block-lists lose to encoding tricks every time.
Use cryptography you don't have to think about
The most common cryptographic mistake in Java is not a broken algorithm; it's using a general-purpose randomness source or an outdated cipher mode. Reach for SecureRandom for anything security-relevant, and standardize on authenticated encryption:
// Session tokens, password reset nonces, etc.
SecureRandom random = new SecureRandom();
byte[] token = new byte[32];
random.nextBytes(token);
// Prefer AES-GCM (authenticated) over AES-ECB/CBC
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
Ban MD5 and SHA-1 for anything security-relevant, hash passwords with a memory-hard function such as Argon2 or bcrypt rather than a bare digest, and require TLS 1.2 as a floor — TLS 1.0 and 1.1 were formally deprecated by IETF RFC 8996 in March 2021. Enforce these with a static-analysis rule, not a wiki page nobody reads.
Treat deserialization as remote code execution
Java's native serialization is the single most dangerous API most teams still expose. A call to readObject() on attacker-controlled bytes can trigger a "gadget chain" that reaches remote code execution without any memory-corruption bug — the ysoserial project catalogs more than twenty working chains across common libraries. Prefer a data format like JSON or Protocol Buffers. Where native deserialization is unavoidable, install a JVM-wide allow-list filter (available since JDK 9, backported to 8u121):
// Only these classes may be deserialized; everything else is rejected
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.acme.dto.*;java.util.*;!*");
ObjectInputFilter.Config.setSerialFilter(filter);
Keep dependencies current, and know what's reachable
Log4Shell's fix shipped in Log4j 2.15.0 within days of disclosure in December 2021, yet scanners were still finding tens of thousands of exposed instances months later. The gap is never the availability of a patch; it's visibility and speed. Generate a software bill of materials on every build, scan it against known CVEs, and patch against a hard SLA (for example, 15 days for CVSS 9.0+). Automated software composition analysis makes this continuous rather than a quarterly fire drill, and it sees the transitive dependencies that never appear in your pom.xml or build.gradle.
Harden the runtime
Java's Security Manager was deprecated for removal in JDK 17 (JEP 411) and removed in JDK 24, so process- and container-level isolation now carries the weight it used to. Run the JVM as a non-root user, use a read-only root filesystem, disable the JNDI class-loading behavior that made Log4Shell so severe (com.sun.jndi.ldap.object.trustURLCodebase has defaulted to false since 8u191), and keep a -Djdk.serialFilter set globally as a defense-in-depth backstop.
Java security checklist
| Phase | Practice | Fails if you skip it |
|---|---|---|
| Input | Parameterized queries, XXE-hardened parsers, allow-list validation | SQLi, XXE, injection |
| Crypto | SecureRandom, AES-GCM, Argon2/bcrypt, TLS 1.2+ | Token prediction, weak hashing |
| Serialization | Avoid native serialization; set ObjectInputFilter | Deserialization RCE |
| Dependencies | Per-build SBOM, CVE scan, patch SLA | Log4Shell-class exposure |
| Runtime | Non-root, read-only FS, JNDI hardening | Privilege escalation |
How Safeguard helps
Safeguard operationalizes the dependency and runtime parts of this list — the parts that cause the most real breaches. Its reachability-aware scanning engine traces whether a vulnerable method in a flagged dependency is actually invokable from your code, so your team patches the small fraction of CVEs that are genuinely exploitable first instead of triaging every CVSS 9.0 alert with equal urgency. Continuous software composition analysis and automatically generated SBOMs give you the live classpath inventory that quarterly scans miss, and when a safe fix exists, auto-fix pull requests open the version bump directly against your repository. You can wire the same checks into local development and CI with the Safeguard CLI, and if you're weighing options, the platform comparison hub lays out how the reachability approach differs from legacy scanners.
Start free at app.safeguard.sh/register, or read the integration guides at docs.safeguard.sh to add Java scanning to your pipeline in an afternoon.