CWE-295: Improper Certificate Validation - Java
Overview
Improper certificate validation in Java occurs when code replaces the JVM's default TrustManager with an implementation that accepts all certificates, disables hostname verification on HttpsURLConnection, or configures an SSLContext with a no-op X509TrustManager. 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) and verifies that the certificate identity matches the hostname. Modern certificates should use Subject Alternative Name (SAN); Common Name fallback is legacy compatibility, not the target state. Replacing this with an empty trust manager 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. Use HttpClient (Java 11+) with default SSL configuration. For internal CA certificates, load the CA into a KeyStore and build a proper TrustManagerFactory - do not bypass validation.
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).
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("JKS");
try (FileInputStream fis = new FileInputStream("internal-ca.jks")) {
trustStore.load(fis, System.getenv("TRUSTSTORE_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, import the internal CA into a copy of the default trust store or configure a trust store that includes both the internal CA and the required public roots. Hostname verification is performed byHttpClientindependently.
Legacy HttpsURLConnection with Default Validation
import javax.net.ssl.HttpsURLConnection;
import java.net.URL;
// SECURE: do NOT set a custom SSLSocketFactory or HostnameVerifier
URL url = new URL("https://api.example.com/data");
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. Simply removing custom assignments is often sufficient.
Remediation Steps
Locate the Finding
- Source: Any outbound HTTPS connection
- Sink:
TrustManagerwith empty methods,HostnameVerifierreturningtrue,SSLContext.init(null, trustAllCerts, null)
Apply the Fix
- PRIORITY 1: Remove all
TrustManagerimplementations with emptycheckServerTrusted()bodies - PRIORITY 2: Remove all
setDefaultHostnameVerifier(... -> true)calls - PRIORITY 3: For internal CAs, create a
KeyStore-backedTrustManagerFactoryinstead of disabling validation
Verify the Fix
- Connect to a host with a self-signed certificate and confirm
SSLHandshakeExceptionis thrown - Confirm connections to valid production endpoints still succeed
- Rescan with the security scanner to confirm the finding is resolved
Check for Similar Issues
Search for: X509TrustManager, HostnameVerifier, setDefaultSSLSocketFactory, SSLContext.init(
Testing
- Normal input: connect to valid public endpoints and internal endpoints whose certificates chain to the configured trust store.
- Boundary input: test expired certificates, hostname mismatches, missing intermediates, and internal CA rotation.
- Malicious input: connect through a proxy presenting a self-signed or wrong-host certificate; confirm the request fails with
SSLHandshakeException.