PHP cURL SSL Peer Verification Disabled

High Risk Insecure Communication
phpcurlsslcertificate-validationmitmhttps

What it is

The PHP application disables SSL peer verification in cURL requests by setting CURLOPT_SSL_VERIFYPEER to false, making it vulnerable to man-in-the-middle attacks. This configuration bypasses certificate validation, allowing attackers to intercept and manipulate HTTPS communications.

// Vulnerable: Disabled SSL peer verification
function makeHttpsRequest($url, $data) {
    $ch = curl_init();
    
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    // Dangerous: Disables SSL certificate verification
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
    
    $response = curl_exec($ch);
    
    if (curl_error($ch)) {
        throw new Exception('cURL error: ' . curl_error($ch));
    }
    
    curl_close($ch);
    return $response;
}

$result = makeHttpsRequest('https://api.example.com/data', $postData);
// Secure: Proper SSL verification enabled
function makeSecureHttpsRequest($url, $data) {
    $ch = curl_init();
    
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    // Secure: Enable SSL verification
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
    
    // Set CA certificate bundle
    curl_setopt($ch, CURLOPT_CAINFO, '/path/to/cacert.pem');
    
    // Additional security options
    curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
    curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
    
    $response = curl_exec($ch);
    
    if (curl_error($ch)) {
        $error = curl_error($ch);
        curl_close($ch);
        throw new Exception('Secure cURL request failed: ' . $error);
    }
    
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($httpCode !== 200) {
        throw new Exception('HTTP request failed with status: ' . $httpCode);
    }
    
    return $response;
}

try {
    $result = makeSecureHttpsRequest('https://api.example.com/data', $postData);
} catch (Exception $e) {
    error_log('HTTPS request error: ' . $e->getMessage());
    // Handle error appropriately
}

💡 Why This Fix Works

The vulnerable code was updated to address the security issue.

Why it happens

Developers encounter SSL certificate verification errors during cURL requests—typically SSL certificate problem: unable to get local issuer certificate or SSL certificate problem: self signed certificate in certificate chain—and immediately disable verification with curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); to make the error disappear and allow the HTTP request to proceed. This quick-fix approach treats the SSL error as an impediment to functionality rather than a critical security warning that the certificate chain cannot be validated. The pattern often appears when integrating with third-party APIs during rapid development cycles where developers prioritize getting the integration working over understanding certificate validation. Stack Overflow searches for SSL certificate errors commonly suggest disabling verification as the first solution, which developers copy without understanding security implications. The disabled verification setting gets committed to version control, passes code review unnoticed, and deploys to production where it leaves HTTPS connections vulnerable to man-in-the-middle attacks. Attackers positioned between the PHP application and the API endpoint can intercept traffic, present a fraudulent certificate (which the application accepts without verification), decrypt sensitive data in transit, modify API responses, or capture authentication credentials.

Root causes

Setting CURLOPT_SSL_VERIFYPEER to False to Bypass Certificate Errors

Developers encounter SSL certificate verification errors during cURL requests—typically SSL certificate problem: unable to get local issuer certificate or SSL certificate problem: self signed certificate in certificate chain—and immediately disable verification with curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); to make the error disappear and allow the HTTP request to proceed. This quick-fix approach treats the SSL error as an impediment to functionality rather than a critical security warning that the certificate chain cannot be validated. The pattern often appears when integrating with third-party APIs during rapid development cycles where developers prioritize getting the integration working over understanding certificate validation. Stack Overflow searches for SSL certificate errors commonly suggest disabling verification as the first solution, which developers copy without understanding security implications. The disabled verification setting gets committed to version control, passes code review unnoticed, and deploys to production where it leaves HTTPS connections vulnerable to man-in-the-middle attacks. Attackers positioned between the PHP application and the API endpoint can intercept traffic, present a fraudulent certificate (which the application accepts without verification), decrypt sensitive data in transit, modify API responses, or capture authentication credentials.

Disabling SSL Verification for Development that Carries to Production

Development environments use self-signed certificates, internal Certificate Authorities, or localhost testing setups that fail standard SSL validation, prompting developers to set curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); to enable local testing. These settings get placed in configuration files (config.php, bootstrap.php), HTTP client wrapper classes, or API integration libraries that are shared across all environments. While developers intend these settings to be temporary development conveniences, they propagate to staging and production deployments through several mechanisms: configuration files without environment-specific overrides (missing if ($_ENV['APP_ENV'] !== 'production') guards), copy-pasted code from development to production branches, shared libraries included in production builds, or deployment scripts that don't differentiate security settings by environment. Code review processes may miss these settings because reviewers see them in development context where they appear necessary. The resulting production deployment accepts any SSL certificate presented by remote servers, enabling attackers to perform SSL stripping attacks, DNS hijacking with fraudulent certificates, or ARP spoofing on local networks to intercept HTTPS connections that should be protected. Payment processing integrations, OAuth authentication flows, and API communications transmitting sensitive data all become vulnerable to interception.

Missing or Improper CA Certificate Bundle Configuration

PHP cURL installations lack properly configured Certificate Authority (CA) certificate bundles (cacert.pem or ca-bundle.crt files) required to validate SSL certificate chains, causing all HTTPS requests to fail with unable to get local issuer certificate errors. Rather than configuring the CA bundle path correctly with curl_setopt($ch, CURLOPT_CAINFO, '/path/to/cacert.pem');, developers disable verification entirely. This issue commonly occurs on Windows development machines where PHP doesn't ship with CA bundles by default, on Docker containers built from minimal base images missing CA certificate packages, in shared hosting environments where PHP's openssl.cafile and curl.cainfo php.ini directives aren't set, or after OpenSSL upgrades that change default certificate locations. Different operating systems store CA certificates in different locations (/etc/ssl/certs/ca-certificates.crt on Debian/Ubuntu, /etc/pki/tls/certs/ca-bundle.crt on RHEL/CentOS, C:\Windows\System32\curl-ca-bundle.crt on Windows), and PHP cURL doesn't automatically find them. Developers unfamiliar with certificate infrastructure treat the missing CA bundle as an environmental quirk rather than a security configuration requirement, opting to disable verification rather than correctly configuring certificate paths. The vulnerability is particularly insidious because HTTPS connections appear to work correctly (encrypted with valid certificates), but the application never validates that the certificates are trustworthy, accepting any certificate including those issued by attackers.

Attempting to Work Around Self-Signed Certificate Issues

Applications integrate with internal APIs, partner systems, or IoT devices using self-signed SSL certificates that fail verification with SSL certificate problem: self signed certificate errors. Developers attempting to maintain HTTPS encryption while working around self-signed certificate issues choose to disable verification with curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); rather than implementing proper self-signed certificate handling. The correct approach involves obtaining the self-signed certificate, adding it to a custom CA bundle, and configuring cURL to trust that specific certificate: curl_setopt($ch, CURLOPT_CAINFO, '/path/to/self-signed-cert.pem'); However, this requires understanding certificate export/import procedures, maintaining certificate files in the application deployment, and updating certificates before expiration—operational complexity that developers avoid by disabling verification. Corporate environments with internal Certificate Authorities face similar issues where internal CA root certificates aren't in system trust stores, requiring explicit configuration. Legacy systems, embedded devices, and development tools often ship with expired or self-signed certificates that administrators don't update. Rather than addressing the root cause (invalid certificates that should be replaced with properly-signed certificates from internal CAs or Let's Encrypt), developers disable verification as a workaround, creating permanent security vulnerabilities justified by temporary integration needs.

Lack of Understanding of SSL Security Implications

Development teams lack security training about SSL/TLS certificate validation, treating CURLOPT_SSL_VERIFYPEER as a troubleshooting setting rather than a critical security control. Developers believe that using https:// URLs provides encryption and security, not understanding that encryption without authentication (certificate verification) provides no protection against man-in-the-middle attacks—attackers can establish their own encrypted connection to both client and server, decrypting and re-encrypting traffic in transit. The misconception that SSL errors indicate problems with the remote server rather than potential security attacks leads developers to disable verification to work around errors instead of investigating their root cause. Documentation and tutorials often fail to explain that CURLOPT_SSL_VERIFYPEER verifies certificate authenticity (preventing impersonation) while CURLOPT_SSL_VERIFYHOST verifies certificate hostname matching (preventing certificate reuse), and both are essential for secure HTTPS. Developers may set CURLOPT_SSL_VERIFYPEER to true but CURLOPT_SSL_VERIFYHOST to 0 or 1 (instead of 2), partially disabling validation. The abstract nature of man-in-the-middle attacks—difficult to reproduce in development environments, requiring attacker positioning on the network path—makes the threat seem theoretical rather than practical, reducing urgency around fixing disabled verification. Security vulnerabilities aren't discovered until penetration testing or security audits, by which time disabled verification has become embedded in production code, third-party integrations, and operational procedures.

Fixes

1

Enable SSL Peer Verification by Setting CURLOPT_SSL_VERIFYPEER to True

Always enable SSL certificate verification in cURL requests by explicitly setting curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); and curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); for all HTTPS connections. CURLOPT_SSL_VERIFYPEER validates that the server's SSL certificate is signed by a trusted Certificate Authority and hasn't expired. CURLOPT_SSL_VERIFYHOST set to 2 (not 1 or 0) verifies that the certificate's Common Name or Subject Alternative Name matches the requested hostname, preventing certificate substitution attacks. Never rely on PHP's defaults—always explicitly set these options to true/2 to ensure verification is enabled even if PHP configuration changes. Create secure-by-default HTTP client wrapper functions that enforce verification: function secureHttpsRequest($url, $options = []) { $ch = curl_init($url); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); /* additional options */ return curl_exec($ch); }. For Guzzle HTTP client, verification is enabled by default, but explicitly configure: $client = new GuzzleHttp\Client(['verify' => true]); or $client->request('GET', $url, ['verify' => true]); Audit all cURL initialization code searching for curl_setopt calls with SSL_VERIFYPEER or SSL_VERIFYHOST: grep -r 'CURLOPT_SSL_VERIFY' to identify insecure configurations. Implement pre-commit git hooks that reject commits containing CURLOPT_SSL_VERIFYPEER => false or CURLOPT_SSL_VERIFYHOST => 0 patterns. Enable static analysis tools like Psalm or PHPStan with security rulesets that flag disabled SSL verification.

2

Configure Proper CA Certificate Bundle with CURLOPT_CAINFO

Configure cURL to use a valid CA certificate bundle for certificate chain validation by setting curl_setopt($ch, CURLOPT_CAINFO, '/path/to/cacert.pem'); Download the official Mozilla CA certificate bundle from https://curl.se/docs/caextract.html (cacert.pem file extracted from Firefox) and store it in your application: curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . '/../certs/cacert.pem'); Update the CA bundle regularly (at least quarterly) to include new CAs and revoke compromised certificates. Alternatively, configure system-wide CA bundles by setting curl.cainfo in php.ini: curl.cainfo = /etc/ssl/certs/ca-certificates.crt or openssl.cafile = /etc/ssl/certs/ca-bundle.crt. For Docker containers, install CA certificates package: RUN apt-get update && apt-get install -y ca-certificates in Dockerfile and PHP will use system certificates automatically. Use CURLOPT_CAPATH to specify a directory containing multiple CA certificates (one per file with specific naming): curl_setopt($ch, CURLOPT_CAPATH, '/etc/ssl/certs/'); on Linux systems with c_rehash-indexed certificate directories. Verify CA bundle configuration works: php -r "\$ch = curl_init('https://www.google.com'); curl_setopt(\$ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt(\$ch, CURLOPT_CAINFO, '/path/to/cacert.pem'); curl_exec(\$ch); echo curl_error(\$ch);" should complete without SSL errors. For internal Certificate Authorities, create custom CA bundles: cat /path/to/public-cas.pem /path/to/internal-ca.pem > combined-ca-bundle.pem then use this combined bundle.

3

Use Valid SSL Certificates from Trusted Certificate Authorities

Replace self-signed certificates and untrusted certificates with valid SSL certificates issued by trusted Certificate Authorities recognized by standard CA bundles. Use Let's Encrypt (free automated certificates) for internet-accessible services: install Certbot and configure automatic certificate renewal with certbot renew --deploy-hook "systemctl reload nginx". For internal services, deploy an internal Certificate Authority using tools like HashiCorp Vault PKI, Step CA, or Microsoft Active Directory Certificate Services, then distribute the internal CA's root certificate to all application servers' trust stores. Ensure all certificates include proper Subject Alternative Names (SANs) covering all domain names/IP addresses used to access the service. Certificates must be valid (not expired, not yet valid), have complete certificate chains (including intermediate certificates), and match the hostname in requests. Monitor certificate expiration dates and implement automated renewal workflows: SSL certificates expire (typically 90 days for Let's Encrypt, 1-2 years for commercial CAs) requiring renewal before expiration to prevent service disruption. Test certificate validity from application servers: openssl s_client -connect api.example.com:443 -CAfile /path/to/cacert.pem shows certificate chain and validation results. For partner/vendor integrations using invalid certificates, work with partners to obtain valid certificates rather than disabling verification—provide technical guidance on Let's Encrypt or offer to share your organization's internal CA for mutual authentication. Document certificate requirements in API integration specifications requiring partners to maintain valid certificates.

4

Implement Proper Certificate Pinning for Enhanced Security

Add certificate pinning to protect against compromised Certificate Authorities or fraudulent certificates by validating specific certificate properties beyond standard CA chain validation. Implement public key pinning by extracting the public key fingerprint from trusted certificates and validating it during connections: $expectedFingerprint = 'sha256//base64encodedHash'; curl_setopt($ch, CURLOPT_PINNEDPUBLICKEY, $expectedFingerprint); This prevents accepting certificates signed by different CAs even if they're otherwise valid. Extract public key hashes using: openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64. Pin intermediate CA certificates rather than leaf certificates to allow certificate rotation without code changes: pin the CA certificate that signs your server certificates. For Guzzle: $client->request('GET', $url, ['curl' => [CURLOPT_PINNEDPUBLICKEY => 'sha256//hash1;sha256//hash2']]); supports multiple pins for rotation. Implement certificate pinning in security-critical integrations: payment processors, authentication providers, internal admin APIs, data synchronization endpoints. Create monitoring for certificate changes: before deploying pinning, log actual certificate fingerprints in production for weeks to understand certificate rotation patterns. Test pinning with intentionally wrong pins in staging to verify it correctly rejects invalid certificates. Document pinned certificates in deployment documentation including hash generation commands and rotation procedures. Update pins before certificate expiration: monitor certificate expiration dates and update pinned hashes in application deployments before certificates rotate. Use certificate pinning as defense-in-depth alongside proper CA verification, not as replacement for CA validation.

5

Handle Certificate Errors Appropriately Without Disabling Verification

When certificate validation fails, investigate and fix the root cause rather than disabling verification. Common certificate errors and proper solutions: SSL certificate problem: unable to get local issuer certificate indicates missing CA bundle—configure CURLOPT_CAINFO with proper CA certificate bundle. SSL certificate problem: self signed certificate requires adding the self-signed certificate to a custom CA bundle: curl_setopt($ch, CURLOPT_CAINFO, '/path/to/custom-ca-bundle.pem'); containing the self-signed certificate. SSL certificate problem: certificate has expired means the remote server's certificate expired—contact the service operator to renew their certificate; never disable verification to work around expired certificates. SSL: certificate subject name does not match target host name indicates hostname mismatch—verify you're connecting to the correct hostname, or obtain a certificate with proper Subject Alternative Names. Implement detailed error logging to diagnose certificate issues: if (curl_errno($ch)) { error_log('cURL error ' . curl_errno($ch) . ': ' . curl_error($ch)); error_log('Certificate info: ' . print_r(curl_getinfo($ch, CURLINFO_CERTINFO), true)); throw new SSLException('SSL verification failed'); }. Enable verbose cURL debugging during troubleshooting (remove in production): curl_setopt($ch, CURLOPT_VERBOSE, true); curl_setopt($ch, CURLOPT_STDERR, fopen('php://temp', 'w+')); Create environment-specific exception handling that fails secure in production: if ($environment === 'production' && $sslError) { throw new SecurityException('SSL verification failed in production'); }. Use SSL Labs SSL Test (https://www.ssllabs.com/ssltest/) to diagnose server-side certificate configuration issues. Never treat certificate errors as minor warnings—they indicate potential security attacks or misconfiguration requiring immediate attention.

6

Use Environment-Specific Configuration for SSL Settings

Implement environment-aware SSL configuration that maintains security in production while allowing development flexibility: $sslVerify = $_ENV['APP_ENV'] === 'production' || $_ENV['APP_ENV'] === 'staging'; curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $sslVerify); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $sslVerify ? 2 : 0); However, prefer enabling verification in all environments and properly configuring development certificates instead of disabling verification in development. Create environment-specific configuration files: config/production.php: ['ssl_verify' => true, 'ca_bundle' => '/etc/ssl/certs/ca-bundle.crt'], config/development.php: ['ssl_verify' => true, 'ca_bundle' => __DIR__ . '/../certs/dev-ca-bundle.pem']. Use environment variables for SSL configuration: SSL_VERIFY_ENABLED=true, SSL_CA_BUNDLE_PATH=/path/to/cacert.pem stored in .env.production and .env.development files. Implement configuration validation on application startup ensuring production environments have verification enabled: if ($_ENV['APP_ENV'] === 'production' && !$config['ssl_verify']) { throw new ConfigurationException('SSL verification must be enabled in production'); }. For local development with self-signed certificates, create a development CA bundle containing both public CAs and self-signed development certificates, enabling verification in all environments. Use feature flags or configuration management (Consul, etcd) to control SSL settings dynamically with audit logging: config changes to disable SSL verification trigger security alerts and require approval. Document environment configuration in deployment guides: list required environment variables, certificate file locations, and validation procedures. Implement automated testing that verifies SSL configuration in each environment: integration tests should fail if production configuration disables verification. Use infrastructure-as-code (Terraform, CloudFormation) to enforce SSL verification in deployment templates preventing manual configuration errors.

Detect This Vulnerability in Your Code

Sourcery automatically identifies php curl ssl peer verification disabled and many other security issues in your codebase.