Skip to content

CWE-295: Improper Certificate Validation

Overview

Improper certificate validation means the application does not correctly verify the SSL/TLS certificate presented by the server it connects to. An attacker on the network path can then impersonate the server, intercept the encrypted traffic in a man-in-the-middle attack, and read credentials or other sensitive data in transit.

A certificate does two jobs in TLS: it authenticates the server, proving it is who it claims to be, and it establishes the encrypted channel. When validation is disabled or done wrongly, the encryption still happens, but it may be to an attacker's server.

Relationship to Other CWEs

MITRE records CWE-295 as the parent of the specific certificate checks below, so the same bypass is reported under the parent or under the check that failed, depending on the tool:

OWASP Classification

A07:2025 - Authentication Failures

Risk

High: A client that accepts a certificate it should reject hands the session to whoever presented it - a man-in-the-middle (MITM). Everything the connection carries is readable to them:

  • Usernames and passwords, as they are sent
  • Session tokens, which can be captured and reused to take over the session
  • Authentication tokens and API keys for external services
  • Credit card numbers and other financial information
  • Any other sensitive data the connection carries

A client that does not validate fails PCI DSS's encryption-in-transit requirement outright; under SOC 2 and HIPAA it is failing a control you asserted you had, or a risk-based safeguard, rather than a flat prohibition.

MITM attacks are easy to run on public WiFi, and a client that skips validation gives the user no warning that one is happening.

Common Validation Failures

Validation failures usually take one of these forms:

  • Disabling validation entirely: Accepting all certificates regardless of validity
  • Hostname verification bypass: Not checking that the hostname matches the certificate
  • Chain verification failure: Not verifying the certificate chain to a trusted root CA
  • Ignoring certificate status: Accepting expired or revoked certificates
  • Missing revocation checks where required: Not enabling or enforcing Certificate Revocation Lists (CRL) or OCSP when your platform, policy, or threat model requires revocation checking

Two of them are worth spelling out, because neither looks like a disabled check.

Chain and hostname are separate, and a client can have one without the other. A client can verify the chain perfectly and still accept a certificate issued for a different name, which authenticates nobody: the attacker needs only a valid certificate for a domain they control, and public CAs issue those for free. This half-disabled state is what survives review, because the code contains a real CA bundle and nothing a verify=False rule matches. It is also the default on raw TLS sockets in several ecosystems - a Java SSLSocket and a Python SSLContext.wrap_socket() called without server_hostname both check the chain and skip the name. Anywhere the application speaks TLS without going through an HTTPS client, confirm the name check is switched on.

Some bypasses never appear in application code. Environment variables and interpreter flags disable validation for the whole process, including connections made inside dependencies: NODE_TLS_REJECT_UNAUTHORIZED=0 and -Djdk.internal.httpclient.disableHostnameVerification=true switch validation off outright, while SSL_CERT_FILE, REQUESTS_CA_BUNDLE, NODE_EXTRA_CA_CERTS and a javax.net.ssl.trustStore path leave it on and change what is trusted - which has the same effect if the file is one an attacker can write. They live in Dockerfiles, CI job definitions, systemd units and shell profiles, so they show up in no diff and trip no scanner rule. Search deployment configuration for them alongside the source, and re-run the verification below against the real entrypoint rather than a developer shell.

Remediation Steps

Core Principle: TLS must validate certificates correctly (chain + hostname); never disable verification in production.

Locate the improper certificate validation

  • Start from the code the finding points at, then check every other place the application opens a TLS connection: API calls to external services, database connections over SSL/TLS, email servers, webhooks
  • Search for bypass patterns: "verify=False", "rejectUnauthorized: false", "TrustAllCertificates", "InsecureTrustManager"
  • Check configuration files such as application.properties for certificate validation settings

Use default certificate validation (Primary Defense)

  • Use the platform's default validation: standard HTTPS libraries validate correctly with default settings. Do not write custom certificate validation logic
  • Use the runtime's default trust store, and find out which one that is: it is not always the operating system's. Java validates against its own cacerts keystore and Node against a bundled copy of the Mozilla root list, so adding a CA to the OS store may not reach either. Whether it does is a packaging question rather than a language one - several Linux distributions wire cacerts into their system CA tooling so that update-ca-certificates or update-ca-trust updates it too, and a JDK installed from an archive or a container image usually has no such link. Do not assume in either direction: add the CA, then make one request from the runtime itself. This mismatch is a routine cause of the bypass being reviewed - an internal endpoint works in a browser and in curl, fails in the application, and validation gets disabled to make it work. See the language-specific pages for each runtime's own mechanism
  • Remove bypass code: delete any "verify=False", "rejectUnauthorized: false", or custom TrustManager that accepts all certificates
  • Never disable validation: even in development, add the development CA or certificate to the trust store instead of switching validation off

Validate certificate chain and hostname properly

If custom validation is unavoidable, it has to check all of the following:

  • The chain leads to a trusted root certificate authority
  • The current time falls between notBefore and notAfter
  • The certificate is not revoked - check a Certificate Revocation List (CRL) or OCSP if revocation checking is required and supported by your client stack
  • The hostname matches a Subject Alternative Name (SAN). Treat Common Name-only certificates as legacy certificates to replace, not as a compatibility target
  • The certificate is issued for server authentication
  • The signature algorithm is SHA-256 or stronger, not MD5 or SHA-1

Remove development bypass code and use pinning only for high-security cases

  • Development shortcuts: Delete "trust all certificates" classes, "ignore SSL errors" flags and custom TrustManagers. Grep for "trust all", "accept all", "disable validation" and "InsecureTrustManager" to find them
  • Environment bypasses: Delete environment variables and debug flags that skip certificate checks
  • Certificate pinning (optional): for critical connections, pin expected public key/SPKI hashes with backup pins and a rotation plan. Do not pin short-lived leaf certificates without operational support

Check Kubernetes and Infrastructure Configuration

Bypasses also live in Kubernetes configuration and tooling, where they are easy to miss:

  • kubeconfig insecure-skip-tls-verify: true disables TLS verification for all API server communication, exposing service account tokens and cluster traffic to MITM attacks - commonly added as a quick fix for certificate errors and left in permanently. Provide the cluster CA certificate via certificate-authority-data instead.
  • kubectl --insecure-skip-tls-verify should never appear in CI/CD pipelines or automation scripts - configure the cluster CA correctly in kubeconfig instead.
  • In-cluster applications should use the service account CA bundle automatically mounted at /var/run/secrets/kubernetes.io/serviceaccount/ca.crt rather than disabling certificate validation when calling the Kubernetes API. See the Go and Python language-specific pages for code-level guidance.

Monitor and audit SSL/TLS connections

  • Log SSL/TLS handshake failures; they can indicate MITM attempts or expired certificates
  • Monitor certificate expiration dates and renew before expiry
  • Reject certificate validation bypasses in code review
  • Use static analysis to detect disabled validation

Verify the fix against a live endpoint

Re-scanning proves the bypass is gone from the source; it cannot tell you whether the client now validates, or whether it still reaches the endpoints it needs to. Both halves need a request.

badssl.com publishes an endpoint for each failure mode, so the rejection tests need no infrastructure:

# Baseline: a valid public certificate must still connect
curl https://badssl.com/                # expect: 200

# Each of these must fail, and the error must name the certificate
curl https://self-signed.badssl.com/    # expect: self-signed certificate
curl https://expired.badssl.com/        # expect: certificate has expired
curl https://wrong.host.badssl.com/     # expect: hostname mismatch

Run them through the application's own HTTP client, not only through curl - a client can be configured quite differently from the shell, and the point is to exercise the code path that carried the finding.

  • Assert the accept, not only the rejects. A client that refuses every certificate passes all three rejection tests. This is the usual way a certificate-validation fix goes wrong: an internal-CA-only trust store that now breaks public endpoints, a custom chain build that cannot find the intermediates, or revocation checking against a CA that publishes no CRL. Each reads as hardening in the diff and fails only against a real endpoint.
  • Test each client the process creates. Applications rarely have one, and hostname verification is configured per client rather than per process - on raw TLS sockets it is off unless switched on. See the language-specific pages.
  • Test the runtime, not just the source. Start the application the way production starts it and repeat the wrong-host test. The environment variables and interpreter flags described on the language pages disable validation with nothing in the source to find, so a fix that holds from a developer shell can be inert under the real entrypoint.
  • https://revoked.badssl.com/ is only expected to fail where revocation checking is both enabled and supported by your client stack. Most default configurations connect to it, and that on its own is not a finding.
  • Re-run the scanner that reported the issue and confirm it no longer triggers.

Migration Considerations

Enabling certificate validation can break integrations that rely on self-signed or expired certificates.

What Breaks

  • Dev/staging environments: Often use self-signed certificates
  • Internal APIs: May have self-signed or corporate CA certificates
  • Legacy systems: Old servers with expired certificates
  • Partner integrations: Third-party APIs with certificate issues
  • Testing environments: Mock servers with invalid certificates

Migration Approach

Gradual Rollout (Recommended)

  • Phase 1: Inventory endpoints and run validation probes outside the live request path using openssl s_client:
    # Probe a single endpoint and report certificate or hostname validation errors
    openssl s_client -connect api.example.com:443 -servername api.example.com \
      -verify_hostname api.example.com -verify_return_error < /dev/null 2>&1 \
      | grep -E "Verify return code|subject|issuer"
    

    See the language-specific pages for programmatic probing patterns.

  • Phase 2: Identify problematic endpoints from logs
    # Analyze logs to find failing certificates
    grep "Certificate validation failed" app.log | sort | uniq -c
    
  • Phase 3: Fix certificate issues or add trusted CAs

    For internal or partner CAs, add the CA certificate to your application's trust store rather than disabling validation. See the language-specific pages for code-level patterns (Python ssl.create_default_context, Java KeyStore/TrustManagerFactory, C# CustomRootTrust or OS trust stores, Go tls.Config.RootCAs, Node.js NODE_EXTRA_CA_CERTS). Check whether the mechanism you pick adds to the default roots or replaces them - several replace, which is how a fix for an internal endpoint breaks every public one.

  • Phase 4: Enable enforcement and remove all bypass flags

    Remove all verify=False, rejectUnauthorized: false, trust-all callbacks, and empty TrustManager implementations. Certificate validation errors should propagate as exceptions, not be swallowed.

Environment-Specific Configuration

Development and staging environments should use a trusted internal CA certificate rather than disabled validation. Configure the CA bundle path via an environment variable and point it to your dev CA file:

# Set a dev CA bundle path - use a real CA cert, never disable verification
export DEV_CA_BUNDLE=/etc/ssl/certs/dev-ca.pem

Add Specific Certificates to Trust Store

Add CA certificates to the system trust store on Linux:

# Add corporate CA to system trust store (Debian/Ubuntu)
cp corporate-ca.crt /usr/local/share/ca-certificates/
update-ca-certificates

This is the starting point, not the whole job, because of the mismatch described above: it reaches clients that read OpenSSL's default paths - curl, Python's ssl module, Go on Linux - and it does not reach Node, which uses its own bundled roots. Whether it reaches Java depends on whether that JDK's cacerts is linked to the system store. It also does not reach requests, which uses the certifi bundle. After running it, make one request from each runtime involved rather than assuming the CA is now trusted everywhere.

For application-level trust stores, refer to the language-specific pages: C# uses CustomRootTrust on supported .NET versions or OS trust stores, Java uses a KeyStore-backed TrustManagerFactory, Python uses ssl.create_default_context with load_verify_locations, Go uses tls.Config.RootCAs, and Node.js uses the ca agent option, or NODE_EXTRA_CA_CERTS where the CA should be added to the defaults rather than replace them.

Rollback Procedures

If certificate validation breaks critical services:

  • Trust the correct CA quickly: add the missing internal or partner CA to the application or system trust store.
  • Route to a known-good endpoint: temporarily move traffic to an endpoint with a valid certificate.
  • Fail closed for sensitive traffic: do not restore verify=False, rejectUnauthorized: false, or trust-all validators as a rollback.

Before Enforcing

The rejection tests are covered under Verify the fix and do not need repeating here. What migration adds is the other direction - the connections that must keep working once bypasses are removed:

  • Every production API the service calls returns its normal response, including any that were reached with validation disabled.
  • Endpoints behind a corporate or partner CA connect with that CA in the trust store, on each environment separately - development, staging and production frequently have different bundles.
  • The inventory of endpoints with non-public certificates is written down, with the CA each one chains to and who renews it. This is what makes the next expiry a ticket rather than an outage, and it is the artefact most often skipped.

Language-Specific Guidance

Each language page shows the vulnerable and secure patterns for its ecosystem:

  • C# / .NET - HttpClientHandler callback bypass, DangerousAcceptAnyServerCertificateValidator, ServicePointManager, custom CA validation
  • Go - crypto/tls InsecureSkipVerify, custom RootCAs, mutual TLS
  • Java - empty X509TrustManager, trust-all HostnameVerifier, SSLContext bypass, HttpClient secure patterns
  • JavaScript/Node.js - https module rejectUnauthorized, axios httpsAgent, node-fetch configuration, TLS module, certificate pinning
  • Python - requests verify=False, urllib3 cert_reqs, SSL context configuration, httpx, aiohttp validation

Additional Resources