Java runs an estimated 60% of enterprise back-end systems and remains the top language behind breaches tied to open-source components. The Equifax breach of March 2017 exposed 147 million records because a Struts2 vulnerability (CVE-2017-5638) sat unpatched for two months after a fix shipped. Four years later, Log4Shell (CVE-2021-44228) hit a CVSS score of 10.0 and forced security teams into emergency patch cycles across nearly every Java shop on earth, from Minecraft servers to Fortune 500 core banking platforms. These weren't exotic zero-days requiring nation-state tooling — they were dependency and deserialization problems that well-known controls prevent. This post lays out 10 concrete Java security best practices, grouped into five questions security and platform teams actually ask, with the CVEs, patch numbers, and configuration flags that make each one actionable rather than theoretical.
What is the leading cause of Java security breaches?
The leading cause is unpatched or unmonitored open-source dependencies, not custom application code. Sonatype's 2023 State of the Software Supply Chain report found the average Java application pulls in 148 open-source dependencies, and Maven Central now hosts over 700,000 published artifacts, most several layers deep in a build's transitive tree. OWASP's Top 10 (2021 revision) ranks "Vulnerable and Outdated Components" as category A06 for exactly this reason.
Best practice 1 — Generate and maintain a live SBOM. Produce a CycloneDX or SPDX software bill of materials on every build, not once a quarter. A quarterly SBOM misses the 30-45 day window between a CVE's public disclosure and its typical private exploitation, which is the window attackers use most.
Best practice 2 — Patch known CVEs against an SLA, not a backlog. Log4Shell affected Log4j 2.0-beta9 through 2.14.1; the fix landed in 2.15.0 within four days of disclosure (December 9-13, 2021), yet Censys was still finding over 34,000 exposed Log4j instances in February 2022. Set a hard SLA — 15 days for CVSS 9.0+, 30 days for 7.0-8.9 — and track it as a security KPI, not a dev-team courtesy.
How do you stop deserialization exploits in Java applications?
You stop them by never deserializing untrusted data with Java's native ObjectInputStream and by disabling JNDI lookups in any library that supports them. Native deserialization gadget chains (the ysoserial toolkit lists over 20 working chains across Commons Collections, Spring, and Groovy) let an attacker turn a single readObject() call into remote code execution without ever needing a memory-corruption bug.
Best practice 3 — Use allow-lists or safe formats instead of native serialization. Prefer JSON (via Jackson with enableDefaultTyping disabled) or Protocol Buffers over java.io.Serializable. Where deserialization is unavoidable, implement ObjectInputFilter (available since JDK 9, backported to 8u121) to allow-list acceptable classes.
Best practice 4 — Lock down JNDI and class-loading behavior. Log4Shell's root cause was the JNDI lookup feature in Log4j's message-substitution syntax (${jndi:ldap://...}), which let a single logged string trigger a remote class load. Set log4j2.formatMsgNoLookups=true on legacy versions and, more broadly, restrict com.sun.jndi.ldap.object.trustURLCodebase to false on every JVM you operate — it has defaulted to false since JDK 6u211/7u201/8u191, but older embedded JVMs still ship it enabled.
Why do XML and injection flaws still appear in modern Java code?
They still appear because default XML parser configurations in Java trust external entities unless a developer explicitly disables them, and because string-concatenated SQL still compiles and runs without warning. XXE (XML External Entity) issues are OWASP A05:2021-adjacent and routinely turn up in SAML consumers, SOAP endpoints, and file-upload handlers that parse XML metadata.
Best practice 5 — Disable external entity resolution on every XML parser. For DocumentBuilderFactory, set FEATURE_SECURE_PROCESSING to true and explicitly disable http://apache.org/xml/features/disallow-doctype-decl. Do this once in a shared factory wrapper class so no team can instantiate an unsafe parser by copy-pasting old code.
Best practice 6 — Use parameterized queries or a vetted ORM, never string concatenation. PreparedStatement with bound parameters, or Hibernate/JPA with named parameters, eliminates classic SQL injection. CVE-2022-36033 (a SQL injection in a widely used Java CMS plugin) is a 2022-era reminder that this "solved" problem keeps resurfacing in custom query-building helper methods that bypass the ORM.
How should Java applications handle secrets and authentication?
Java applications should pull secrets from a vault or secrets manager at runtime and should never rely on Java's legacy cryptographic defaults. Hardcoded credentials in Java source and config files (application.properties, web.xml) are consistently among the top findings in static analysis scans of enterprise codebases, and they survive in git history long after rotation.
Best practice 7 — Externalize secrets to a vault with short-lived tokens. Use HashiCorp Vault, AWS Secrets Manager, or Kubernetes Secrets with a sidecar injector rather than environment variables baked into a Docker image layer — baked-in values persist in image history even after redeployment.
Best practice 8 — Enforce modern cryptography and disable insecure defaults. Java's SecureRandom defaults to a strong algorithm, but plenty of legacy code still calls java.util.Random for token generation or uses AES in ECB mode. Standardize on AES-GCM, require TLS 1.2 minimum (TLS 1.0/1.1 were formally deprecated by IETF RFC 8996 in March 2021), and ban MD5/SHA-1 for anything security-relevant via a linting rule, not a wiki page.
What build and runtime controls reduce Java supply chain risk?
They reduce risk by verifying what actually gets pulled into a build and by constraining what a running JVM is allowed to do. The 2021 ua-parser-js-style and, closer to Java, the 2022 poisoned "faker.js"-adjacent npm incidents showed how a compromised transitive dependency can execute arbitrary code at build time — Maven and Gradle builds are equally exposed when plugins fetch and execute code during the build lifecycle.
Best practice 9 — Pin dependency versions and verify artifact checksums/signatures. Use Maven's dependencyManagement block or Gradle's version catalogs to pin exact versions, and enable checksum verification (<verifyChecksum> or --offline reproducible builds) so a compromised mirror can't silently swap a JAR.
Best practice 10 — Harden the runtime with least-privilege containers and modern JVM flags. Java's built-in Security Manager was deprecated for removal starting JDK 17 (JEP 411) and removed entirely in JDK 24, so container-level controls now carry more weight: run as a non-root user, use read-only root filesystems, and enable -Djdk.serialFilter globally as a defense-in-depth backstop even where you've already removed native deserialization from application code.
How Safeguard Helps
Safeguard turns these 10 practices into an enforceable pipeline instead of a checklist. Our reachability analysis traces whether a vulnerable method in a flagged dependency — like the JNDI lookup path in Log4Shell or a gadget-chain class from an outdated Commons Collections version — is actually reachable from your application's code paths, so teams patch the 5-10% of CVEs that matter first instead of triaging every CVSS 9.0 alert equally. Griffin AI reviews the surrounding call stack and business logic to explain why a finding is or isn't exploitable in plain language, cutting down the back-and-forth between security and Java platform teams. Safeguard generates CycloneDX SBOMs automatically on every build and ingests existing SBOMs from your CI pipeline, giving you the continuous inventory that quarterly scans can't. When a fix is available, Safeguard opens an auto-fix pull request with the minimal version bump or configuration change already applied, so remediation ships in minutes rather than waiting for the next sprint planning cycle.