CWE-295: Improper Certificate Validation - Java
Overview
Improper certificate validation in Java occurs when code installs an X509TrustManager that accepts every certificate - on one SSLContext or as the JVM default - or disables hostname verification on HttpsURLConnection. These patterns are common as development workarounds for self-signed certificates that are never removed before deployment.
The JVM's default trust chain validates the server's certificate against the Java cacerts keystore, a well-maintained set of trusted root CAs. cacerts is a distinct store from the operating system's, and whether the two are connected depends on how the JDK was installed rather than on Java itself: several Linux distributions ship a cacerts wired into their system CA tooling, so update-ca-certificates or update-ca-trust updates it too, while a JDK unpacked from an archive or baked into a container image usually has no such link. Do not assume either way - add the CA, then make one request from the JVM and see. Hostname verification is a separate step and is not part of that default: HttpsURLConnection and java.net.http.HttpClient perform it, while a raw SSLSocket or SSLEngine does not until endpoint identification is switched on. See Raw SSLSocket with No Endpoint Identification below. Modern certificates should use Subject Alternative Name (SAN); Common Name fallback is legacy compatibility, not the target state. Replacing the default trust manager with an empty one eliminates the trust-chain check and often appears alongside a hostname-verifier bypass, allowing a man-in-the-middle attacker to present any certificate and silently intercept the decrypted traffic.
Primary Defence: Remove all custom trust managers and hostname verifiers, and use HttpClient (Java 11+) with its default SSL configuration. For internal CA certificates, load the CA into a KeyStore and build a proper TrustManagerFactory - do not bypass validation. Where the code talks to a raw SSLSocket rather than through an HTTP client, removing the bypass is not enough: hostname verification is off by default on a socket and has to be switched on with SSLParameters.setEndpointIdentificationAlgorithm("HTTPS").
Common Vulnerable Patterns
Empty X509TrustManager
// VULNERABLE - empty trust manager accepts every certificate
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { return null; }
public void checkClientTrusted(X509Certificate[] certs, String authType) { } // no-op
public void checkServerTrusted(X509Certificate[] certs, String authType) { } // no-op
}
};
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
Why this is vulnerable:
checkServerTrusted()is the method where certificate chain validation occurs. Leaving it empty means no validation happens - any certificate is accepted, including an attacker's.
Trust-All HostnameVerifier
// VULNERABLE - disables hostname verification globally
HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);
Why this is vulnerable:
- Even with valid chain validation, a certificate for
evil.attacker.comcould be used to impersonateapi.example.com. Hostname verification ensures the certificate was issued for the domain being connected to. Returningtrueunconditionally removes this check.
Combining Both Bypasses
// VULNERABLE - both chain validation and hostname verification disabled
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, null);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(sc.getSocketFactory());
conn.setHostnameVerifier((host, session) -> true);
Why this is vulnerable:
- Disabling both checks means an attacker with network access can intercept any HTTPS connection by presenting a certificate for any domain issued by any CA (or a self-signed certificate).
Apache HttpClient Trust-All Strategy
// VULNERABLE - accepts any chain and skips the hostname check (HttpClient 5.4+)
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(null, TrustAllStrategy.INSTANCE) // or TrustSelfSignedStrategy
.build();
TlsSocketStrategy tls = ClientTlsStrategyBuilder.create()
.setSslContext(sslContext)
.setHostnameVerifier(NoopHostnameVerifier.INSTANCE)
.buildClassic();
CloseableHttpClient client = HttpClients.custom()
.setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create()
.setTlsSocketStrategy(tls).build())
.build();
Why this is vulnerable:
TrustAllStrategyis anX509TrustManagerthat returnstruefor every chain, andNoopHostnameVerifieris the trust-allHostnameVerifierunder a name that does not read as one. Together they are the empty-trust-manager pattern above, assembled from library classes, so the code contains no anonymous inner class for a reviewer to notice.TrustSelfSignedStrategylooks narrower and is not: it accepts any self-signed certificate, including one the attacker generated a moment ago, because "self-signed" is a property of the certificate and not a statement about who issued it. If the goal is to trust one specific development certificate, load that certificate into aKeyStoreand pass it toloadTrustMaterialas the trust store.- The three class names are what to search for; the wiring around them moved in 5.4, where
ClientTlsStrategyBuilder.build()/setTlsStrategy()becamebuildClassic()/setTlsSocketStrategy(). Older code will read.build()and.setTlsStrategy(tls)with aTlsStrategy, and is the same weakness. HttpClient 4.x expresses it asSSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE).
Raw SSLSocket with No Endpoint Identification
// VULNERABLE - no custom TrustManager, and still no hostname check
SSLSocket socket = (SSLSocket) SSLSocketFactory.getDefault()
.createSocket("api.example.com", 443);
socket.startHandshake();
socket.getOutputStream().write(credentials);
Why this is vulnerable:
- Nothing here is disabled. The default
SSLSocketFactoryuses the JVMcacertstrust store and verifies the chain, so the code passes every review that asks "is there a trust-allTrustManager?" - and it never checks that the certificate was issued forapi.example.com. Hostname verification in the JSSE lives onSSLParameters, not in the trust manager, and it is off unless something turns it on.HttpsURLConnectionandjava.net.http.HttpClientturn it on for you; a socket you create yourself does not. Verified on JDK 26: this snippet completes the handshake againstwrong.host.badssl.com, whose certificate names neither that host nor anything like it. - Any certificate from any public CA is therefore accepted, so the attacker needs only a valid certificate for a domain they own. This is the failure mode behind hand-written TLS clients: LDAPS and SMTP-over-TLS helpers, JDBC drivers configured through raw sockets, and anything wrapping a protocol the JDK has no HTTPS-shaped client for.
JVM Flags That Disable Verification
# VULNERABLE - turns off hostname verification for java.net.http.HttpClient
java -Djdk.internal.httpclient.disableHostnameVerification=true -jar app.jar
# VULNERABLE - every JSSE client now trusts whatever is in that keystore
java -Djavax.net.ssl.trustStore=/opt/app/extra-roots.p12 \
-Djavax.net.ssl.trustStorePassword=changeit -jar app.jar
Why this is vulnerable:
jdk.internal.httpclient.disableHostnameVerificationis read by the JDK's ownHttpClientand, when set, accepts a certificate issued for any name. Verified on JDK 26: with the flag, a request tohttps://wrong.host.badssl.com/returns200; without it, the same request throwsSSLHandshakeException. There is no application code to review and no scanner rule to trip - it appears in a container entrypoint, aJAVA_TOOL_OPTIONSvalue or a systemd unit.javax.net.ssl.trustStorereplacescacertsfor every JSSE client in the process. Validation stays on and the certificate is genuinely verified - against whatever roots that file holds, which is the more useful attack: nothing fails, nothing warns, and one certificate signed by the added CA reads every connection. The same command is also the legitimate way to ship a corporate CA, so this is a review question rather than a grep. Ask where the keystore comes from and who can write it, and check what is in it withkeytool -list -keystore.- Grep deployment manifests, Dockerfiles, CI job definitions and
JAVA_TOOL_OPTIONSfor these, not just source. Removing the flag is the whole fix; there is no code change to make.
Secure Patterns
Default HttpClient (Java 11+)
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
// SECURE - default HttpClient validates certificates against the JVM trust store
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/data"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
Why this works:
HttpClient.newHttpClient()uses the JVM defaultSSLContext, which loads thecacertstrust store and enables hostname verification. An invalid certificate throwsSSLHandshakeException.
Custom Internal CA via KeyStore
import javax.net.ssl.*;
import java.security.KeyStore;
import java.io.FileInputStream;
import java.net.http.HttpClient;
// SECURE - load a trust store containing the internal CA and any other roots this client needs
KeyStore trustStore = KeyStore.getInstance("PKCS12"); // JDK default since Java 9; JKS is the proprietary legacy format
try (FileInputStream fis = new FileInputStream("internal-ca.p12")) {
String password = System.getenv("TRUSTSTORE_PASSWORD");
if (password == null) {
throw new IllegalStateException("TRUSTSTORE_PASSWORD is not set");
}
trustStore.load(fis, password.toCharArray());
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm()); // "PKIX"
tmf.init(trustStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
// SECURE - HttpClient with custom trust store; hostname verification still active
HttpClient client = HttpClient.newBuilder()
.sslContext(sslContext)
.build();
Why this works:
- The
TrustManagerFactoryvalidates certificates against the suppliedKeyStore. If this file contains only the internal CA, only certificates chaining to that CA are trusted. If the same client also calls public endpoints, give it a trust store holding both the internal CA and the required public roots - a copy of the default trust store with the CA imported into it. Hostname verification is performed byHttpClientindependently. - How the
.p12was made matters. The JDK treats a PKCS12 certificate as a trust anchor only when the entry carries the trusted-certificate attribute thatkeytool -importcertwrites. A file holding the CA as a plain certificate bag - what most non-Java tooling produces, and whatopenssl pkcs12 -export -nokeysproduces without the-jdktrustoption OpenSSL 3.2 added for exactly this - loads without complaint and then fails on the first handshake withInvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty. Measured on JDK 26 with both kinds of file: the same CA, accepted from one and invisible in the other. Build the store withkeytool, or check it withkeytool -listand expecttrustedCertEntry.
Raw SSLSocket with Endpoint Identification Enabled
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
// SECURE - switch on hostname verification before the handshake
SSLSocket socket = (SSLSocket) SSLSocketFactory.getDefault()
.createSocket("api.example.com", 443);
SSLParameters params = socket.getSSLParameters();
params.setEndpointIdentificationAlgorithm("HTTPS"); // off by default on a socket
socket.setSSLParameters(params);
socket.startHandshake(); // throws SSLHandshakeException on a name mismatch
Why this works:
setEndpointIdentificationAlgorithm("HTTPS")makes the JSSE apply RFC 2818 name matching during the handshake, comparing the peer certificate's Subject Alternative Names against the name the socket was created for. The check runs insidestartHandshake(), so a mismatch fails before any application data is written - which is the property that matters, since a verifier called after the handshake has already let the credentials out.- Set it on the
SSLParametersbeforestartHandshake(). Measured on JDK 26: callingsetSSLParameters()afterwards raises nothing and leaves the already-negotiated session in place, so the wrong-host connection stays open and the code reads as protected while the first exchange went out unverified. - Pass the hostname, not an IP address, to
createSocket(). An IP literal is not sent as SNI, so the server may not even serve the certificate you expected - connecting to a resolved address ofwww.google.comon JDK 26 fails withPKIX path building failedrather than a name mismatch, which sends you debugging the trust store instead of the address. If you must connect to a specific address, open a plainSocketto it and wrap it withSSLSocketFactory.createSocket(Socket, host, port, autoClose), passing the intended hostname; that path verified successfully in the same run.
Legacy HttpsURLConnection with Default Validation
import javax.net.ssl.HttpsURLConnection;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
// SECURE - do NOT set a custom SSLSocketFactory or HostnameVerifier
// new URL(String) is deprecated since Java 20
URL url = URI.create("https://api.example.com/data").toURL();
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setRequestMethod("GET");
try (InputStream in = conn.getInputStream()) {
// connection validated by default JVM trust store
}
Why this works:
HttpsURLConnectionuses the JVM default trust chain and hostname verifier when no custom ones are assigned, so removing the custom assignments is often the whole fix.
Testing
- A request to a valid public endpoint, and one to an internal endpoint whose certificate chains to the configured trust store, both return
200. A trust store holding only the internal CA is the common way to make a fix reject every public endpoint as well, and the failing-request tests below pass either way. https://wrong.host.badssl.com/throwsSSLHandshakeExceptionwithNo subject alternative DNS name matching .... Run this against every TLS client the application creates, including any rawSSLSocket, not only the one the finding named - a socket withoutsetEndpointIdentificationAlgorithm("HTTPS")completes the handshake here and returns data.https://self-signed.badssl.com/andhttps://expired.badssl.com/both throwSSLHandshakeException. Assert on the exception, not on the request failing: a misconfigured trust store fails with the same symptom for a different reason.- Start the application with
-Djdk.internal.httpclient.disableHostnameVerification=trueand confirm the wrong-host request still fails. If it succeeds, that flag is reachable in your deployment and the fix depends on nobody setting it. - Re-run the scanner that reported the finding and confirm it no longer triggers.