Choose your stack

Node.js · Express

Fix TLS certificate validation in Node.js and Express

Never turn verification off. Give the partner client an https.Agent whose ca is the partner's private CA bundle, set rejectUnauthorized: true explicitly so NODE_TLS_REJECT_UNAUTHORIZED=0 cannot flip it, and keep the default checkServerIdentity.

Open the fix card
Python · Flask

Fix TLS certificate validation in Python and Flask

Never pass verify=False. Pass verify= the path of the partner's private CA bundle on every request: requests then checks the chain against that CA alone, the dates and the host name, and a per-request value cannot be replaced by REQUESTS_CA_BUNDLE.

Open the fix card
Java · Spring Boot

Fix TLS certificate validation in Java and Spring Boot

Never install a TrustManager or HostnameVerifier that accepts everything.

Open the fix card
C# · ASP.NET Core

Fix TLS certificate validation in C# and ASP.NET Core

Never return true from a certificate validation callback. Leave validation to SslStream and change only the trust anchor: set SocketsHttpHandler.SslOptions.CertificateChainPolicy to CustomRootTrust with the partner's CA in CustomTrustStore, and RevocationMode NoCheck if the private CA has no CRL.

Open the fix card
Go · net/http

Fix TLS certificate validation in Go and net/http

Leave verification on and change only the trust anchor: put the partner's CA in x509.NewCertPool(), set it as tls.Config.RootCAs with MinVersion tls.VersionTLS12, and keep InsecureSkipVerify false and ServerName empty so crypto/tls checks the chain, the dates and the host name.

Open the fix card

Stacks without a card

How it happens

  • Verification fails in a test or staging environment (a self-signed or private-CA certificate, or a proxy that intercepts TLS), and the quickest change that makes the error go away is to turn verification off: rejectUnauthorized: false, verify=False, a TrustManager with empty methods, or a validation callback that returns true. The setting then ships to production, where every certificate is accepted.
  • Process-wide switches do the same out of sight: NODE_TLS_REJECT_UNAUTHORIZED=0 left in a Dockerfile or .env file, the JVM flag jdk.internal.httpclient.disableHostnameVerification, or ServicePointManager.ServerCertificateValidationCallback in .NET Framework code. Code that relies on the default inherits them.
  • Host name checking is turned off to silence a mismatch, because the service is reached through an IP address or an internal alias. The chain is still checked, but now any certificate from a trusted CA passes for any host. With the public roots that means any certificate any public CA has issued, and an attacker can get one for a domain they own in minutes.
  • Hand-written validation replaces the platform's: a certificate is accepted because its subject CN, or its issuer name, matches an expected string. Names prove nothing. Anyone can create a certificate with any subject and any issuer name; only a signature chain to a trusted key shows who issued it, and RFC 9525 says the CN must not be used to identify a service at all.
  • Trust is widened by accident: a private CA is added to the trust store of every process on the host, or an environment variable such as REQUESTS_CA_BUNDLE quietly replaces the bundle a client was meant to pin.

Fix principles

  • Never disable verification, in code or through the environment. Where an environment variable can change a default, as NODE_TLS_REJECT_UNAUTHORIZED can in Node, set the secure value explicitly on the client.
  • Change trust anchors, not validation logic. For a public service, use the default trust store. For a private one, configure the client for that service to trust exactly the private CA bundle (Node's ca option, requests' verify=<bundle>, a Spring Boot SSL bundle, .NET's CustomRootTrust) and leave every check to the platform.
  • Keep host name verification on. The certificate's subjectAltName must match the host in the URL. If a service is reached under another name, get a certificate that includes that name, or connect by the name the certificate carries.
  • Do not write certificate validation callbacks or trust managers. If one is unavoidable, start from the platform's verdict (SslPolicyErrors.None, the default PKIX trust manager) and only ever add conditions to it; never remove an error.
  • Scope custom trust to the one client that needs it, so other outgoing calls keep the public roots and the private CA cannot vouch for public hosts.
  • Plan for rotation and revocation: let the CA file hold more than one CA while a CA is replaced, and publish and check a CRL or OCSP responder if the private PKI has one.

The scenario

A storefront backend shows exchange rates from a partner API at https://api.partner.test/v1/rates, called with a bearer token. The partner runs its own private CA (Partner Test Root CA), so the client has to be told to trust it. Each test generates a throwaway PKI at setup, runs one loopback HTTPS server at the partner's address and has it present one certificate: the partner's real certificate; a self-signed certificate for api.partner.test; a certificate from the trusted CA for billing.partner.test; a certificate for api.partner.test from an unknown CA that copies the trusted CA's name; or an expired certificate from the trusted CA. GET /rates must return the partner's rates for the first and 502 for every other, with no request, and so no token, reaching the server. The tests map *.partner.test to 127.0.0.1 and change nothing else about TLS.

How the cards prove it

  • Run each card's tests in three modes: the fixed implementation passes, and the vulnerable and the plausible-but-wrong implementations fail. run-all.mjs does this for every card.
  • The exploit tests present a self-signed certificate for the right name, a trusted-CA certificate for another name, a certificate from an unknown CA that copies the trusted CA's name, and an expired certificate. Each must end in 502 with no request, and so no bearer token, reaching the server. The functional test checks that the partner's real certificate is accepted and the token sent. Node and Python add a test that an environment setting (NODE_TLS_REJECT_UNAUTHORIZED=0, REQUESTS_CA_BUNDLE) does not loosen the fix.
  • Against your own service, point the client at a server with a self-signed certificate for the real name, and at one with a valid certificate for a different name (openssl s_server or an intercepting proxy will do). Both handshakes must fail.
  • Static analysis catches only the blunt forms. The rules checked here flag verify=False, rejectUnauthorized: false, an empty TrustManager and a callback that returns true. None of them reported the wrong implementations' real flaw, a disabled host name check (Node's checkServerIdentity, assert_hostname=False set in a requests adapter) or a check that only compares the CN, and .NET's CA5359 misses HttpClientHandler.ServerCertificateCustomValidationCallback returning true. Each card's scannerRules records what was run and what it found.

Identifiers

CWE-295 (improper certificate validation) is the parent and describes the vulnerable implementations, which accept any certificate. CWE-297 (host mismatch) is the Node, Python and Go wrong implementations, which check the chain but not the name. CWE-296 (improper following of the chain of trust) is the Java and C# wrong implementations, which trust any certificate whose subject names the partner, whoever signed it and whenever it expired. OWASP Top 10:2025 maps CWE-295 and CWE-297 to A07 (Authentication Failures) and CWE-296 to A04 (Cryptographic Failures). CAPEC-459 and CAPEC-475 are the two attack patterns MITRE links from CWE-295; CAPEC-94 (Adversary in the Middle) is the attack every exploit test simulates, although its own list of related weaknesses does not name CWE-295. ASVS 5.0.0 requires TLS clients to validate certificates (12.3.2) and internal services to trust only specific internal CAs (12.3.4). OWASP's 2025 A07 page lists CWE-298 and CWE-299 under the title of CWE-297, a copying slip in the source; the IDs used here come from MITRE.

Attack patterns:

References

  1. OWASP Transport Layer Security Cheat Sheet
  2. OWASP Pinning Cheat Sheet
  3. OWASP Top 10:2025 A07 Authentication Failures
  4. OWASP Top 10:2025 A04 Cryptographic Failures
  5. OWASP ASVS 5.0.0, V12 Secure Communication (12.3.2, 12.3.4)
  6. CWE-295: Improper Certificate Validation
  7. CWE-297: Improper Validation of Certificate with Host Mismatch
  8. CWE-296: Improper Following of a Certificate's Chain of Trust
  9. CAPEC-94: Adversary in the Middle (AiTM)
  10. RFC 9525: Service Identity in TLS (the CN must not identify a service)
  11. RFC 5280: Internet X.509 PKI Certificate and CRL Profile
  12. Georgiev et al., The Most Dangerous Code in the World: Validating SSL Certificates in Non-Browser Software (ACM CCS 2012)

Guides on this weakness

Case study

Flame: MD5 certificate forgery

Follow how a weak digest and an overpowered certificate trust path enabled forged code-signing authority, without equating the event with RSA key theft.

Guide

Secure WebRTC signaling, TURN relays and media servers

WebRTC encrypts media, but signaling decides whose keys you trust and TURN relays wherever it is allowed. Authorize every signaling message, mint short-lived TURN credentials, deny internal addresses and harden media servers.

Guide

Review mobile storage, links and platform permissions

Keep local artifacts and platform entry points from exposing authority that belongs on the server.

Guide

Authenticate workloads and authorize their service actions

Use service identities, mutual TLS and function permissions without mistaking an encrypted connection for complete authorization.

Guide

Operate TLS and certificates: configuration, renewal and verification

Configure a modern TLS profile with hybrid post-quantum key exchange, automate renewal for certificate lifetimes that fall to 47 days, and verify every termination point with a fresh handshake.

Guide

Harden an Android app's storage, network and entry points

Encrypt local secrets with Android Keystore keys now that Jetpack Security is deprecated, set transport policy in Network Security Configuration, close exported components and mutable PendingIntents, confine WebViews, verify App Links and judge Play Integrity verdicts on the server, mapped to OWASP MASVS 2.1.

Guide

Harden an iOS app's storage, network and entry points

Store secrets in the Keychain with the narrowest accessibility class, set file protection explicitly, keep App Transport Security on, prefer universal links, constrain WKWebView bridges, keep step-up keys in the Secure Enclave, verify App Attest on the server and ship an accurate privacy manifest, mapped to OWASP MASVS 2.1.

Case study

Case study: certificate expiry outages at Ericsson (2018) and Microsoft Teams (2020), and Equifax's blind spot

How an expired certificate inside Ericsson's core-network software cut mobile data for millions in about 11 countries, how an expired authentication certificate took Microsoft Teams offline, how an expired inspection certificate left Equifax blind for 19 months, through its 2017 breach, and the inventory, automation and testing that prevent each.

Guide

Consume third-party APIs safely (OWASP API10:2023)

Treat every upstream and third-party API response as untrusted input: validate it against a schema, set connect and read timeouts, cap the response size, refuse to follow redirects blindly, keep TLS verification on, and scope the credentials you hold for each provider.

Guide

Worked threat model: a mobile banking app

A complete worked threat model for a mobile banking-style app: the data-flow diagram and its elements, STRIDE threats on the client, the transport and the backend, and the mitigations mapped to OWASP MASVS v2 controls and current Android and iOS platform APIs.