Skip to content

CWE-295: Improper Certificate Validation - C# / .NET

Overview

Improper certificate validation in C# occurs when HttpClientHandler.ServerCertificateCustomValidationCallback is set to a delegate that returns true unconditionally, or when the legacy ServicePointManager.ServerCertificateValidationCallback is assigned a bypass. These patterns completely disable TLS certificate chain and hostname verification, making all HTTPS traffic transparent to any network-level attacker who can intercept the connection.

This flaw is common in development workarounds (e.g., testing against self-signed certificates) that are never removed before the code reaches production. The presence of the word "Dangerous" in HttpClientHandler.DangerousAcceptAnyServerCertificateValidator is intentional - it serves as a warning that this property should never appear in production code.

Primary Defence: Remove the custom callback entirely. HttpClient performs correct certificate chain and hostname validation by default against the Windows/system certificate store. For internal CA certificates, install the CA into the OS or application trust store, or use CustomRootTrust on supported .NET versions - do not bypass validation.

Common Vulnerable Patterns

Trust-All Callback

// VULNERABLE - returns true for every certificate regardless of errors
var handler = new HttpClientHandler
{
    ServerCertificateCustomValidationCallback =
        (message, cert, chain, errors) => true
};
var client = new HttpClient(handler);

Why this is vulnerable:

  • The callback ignores errors, chain, and the certificate's subject/issuer. An attacker with network access can present any certificate and the client will accept it, decrypting the TLS session without the application's knowledge.

DangerousAcceptAnyServerCertificateValidator

// VULNERABLE - named "Dangerous" for a reason; never use in production
var handler = new HttpClientHandler
{
    ServerCertificateCustomValidationCallback =
        HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
};

Why this is vulnerable:

  • This is identical to the trust-all lambda above. The API was deliberately named with "Dangerous" to make code review easier, but developers sometimes dismiss it.

Legacy ServicePointManager Bypass

// VULNERABLE - .NET Framework pattern; disables validation globally
System.Net.ServicePointManager.ServerCertificateValidationCallback +=
    (sender, certificate, chain, sslPolicyErrors) => true;

// All subsequent HttpWebRequest and WebClient calls skip validation
var client = new WebClient();
var data = client.DownloadString("https://api.example.com/data");

Why this is vulnerable:

  • ServicePointManager.ServerCertificateValidationCallback is a process-wide setting. Setting it to a bypass affects every outbound HTTPS call in the application, including calls from third-party libraries.

Secure Patterns

Default HttpClient (No Callback)

using System.Net.Http;

// SECURE: no custom callback - validation uses the Windows/system certificate store
var client = new HttpClient();
var response = await client.GetAsync("https://api.example.com/data");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();

Why this works:

  • The default HttpClientHandler validates the certificate chain against the system trust store and checks the hostname. An invalid certificate causes HttpRequestException with an inner AuthenticationException.

Leaf Certificate Pinning (Narrow Exception)

using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

// SECURE only for a single, fixed endpoint: require normal validation, then pin the leaf certificate
var handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
{
    // Pinning is an additional control, not a replacement for chain, expiry, or hostname validation.
    if (errors != SslPolicyErrors.None || cert is null)
        return false;

    // Accept only one known leaf certificate thumbprint for this endpoint
    const string expectedLeafThumbprint =
        "AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899"; // SHA-256, uppercase, no spaces
    return string.Equals(
        cert.GetCertHashString(System.Security.Cryptography.HashAlgorithmName.SHA256),
        expectedLeafThumbprint,
        StringComparison.OrdinalIgnoreCase);
};

var client = new HttpClient(handler);

Why this works:

  • This keeps the platform's chain, expiry, purpose, and hostname validation enforced, then adds a narrow leaf-certificate pin. Use this only for tightly scoped pinning with rotation procedures. It is not a general internal CA pattern, because a leaf thumbprint changes on normal certificate renewal.

Custom CA via CustomRootTrust (Preferred for Internal PKI on .NET 5+)

using System.Net.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

// SECURE: validate chain errors against one explicitly trusted internal root CA
var handler = new HttpClientHandler();
var internalCa = LoadInternalCACert(); // load from a protected file/store
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
{
    if (errors == SslPolicyErrors.None) return true;

    // A custom chain can only fix trust-chain errors, not hostname or missing cert errors
    if ((errors & ~SslPolicyErrors.RemoteCertificateChainErrors) != 0 || cert is null)
        return false;

    using var customChain = new X509Chain();
    customChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
    customChain.ChainPolicy.CustomTrustStore.Add(internalCa);
    customChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
    customChain.ChainPolicy.ApplicationPolicy.Add(
        new Oid("1.3.6.1.5.5.7.3.1")); // Server Authentication
    // Set revocation mode according to your PKI and policy; do not disable it as a shortcut.
    customChain.ChainPolicy.RevocationMode = X509RevocationMode.Online;

    return customChain.Build(new X509Certificate2(cert));
};

Why this works:

  • The callback only handles RemoteCertificateChainErrors. Hostname mismatch and missing-certificate errors still fail. The custom chain uses CustomRootTrust, so the server certificate must chain to the explicitly loaded internal CA and must be valid for server authentication.
  • For .NET Framework or older .NET versions that do not support CustomRootTrust, install the internal CA into the appropriate OS or application trust store instead of accepting unknown roots in the callback.

Remediation Steps

Locate the Finding

  • Source: Any outbound HTTPS request
  • Sink: ServerCertificateCustomValidationCallback = ... => true, DangerousAcceptAnyServerCertificateValidator, ServicePointManager.ServerCertificateValidationCallback += ... => true

Apply the Fix

  • PRIORITY 1: Remove the ServerCertificateCustomValidationCallback assignment entirely - HttpClient validates correctly by default
  • PRIORITY 2: If removing causes errors because an internal CA is not trusted, add that CA to the OS/application trust store or use a CustomRootTrust callback that still rejects hostname and missing-certificate errors
  • PRIORITY 3: Replace legacy ServicePointManager + WebClient/HttpWebRequest patterns with HttpClient

Verify the Fix

  • Make a request to a host with a self-signed certificate and confirm an HttpRequestException is thrown
  • Confirm that requests to legitimate production endpoints still succeed
  • Rescan with the security scanner to confirm the finding is resolved

Check for Similar Issues

Search for: ServerCertificateCustomValidationCallback, DangerousAcceptAnyServerCertificateValidator, ServicePointManager.ServerCertificateValidationCallback

Testing

  • Normal input: connect to valid production 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 HttpRequestException.

Additional Resources