Skip to content

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.com could be used to impersonate api.example.com. Hostname verification ensures the certificate was issued for the domain being connected to. Returning true unconditionally 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 default SSLContext, which loads the cacerts trust store and enables hostname verification. An invalid certificate throws SSLHandshakeException.

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 TrustManagerFactory validates certificates against the supplied KeyStore. 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 by HttpClient independently.

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:

  • HttpsURLConnection uses 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: TrustManager with empty methods, HostnameVerifier returning true, SSLContext.init(null, trustAllCerts, null)

Apply the Fix

  • PRIORITY 1: Remove all TrustManager implementations with empty checkServerTrusted() bodies
  • PRIORITY 2: Remove all setDefaultHostnameVerifier(... -> true) calls
  • PRIORITY 3: For internal CAs, create a KeyStore-backed TrustManagerFactory instead of disabling validation

Verify the Fix

  • Connect to a host with a self-signed certificate and confirm SSLHandshakeException is 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.

Additional Resources