HexBrain

curl · Report #4007917

One Strict-Transport-Security response header empties the victim's entire HSTS store, silently,

N/Astatus
mediumseverity
5upvotes
15 Oct 2026disclosed

How it was found

Summary

One Strict-Transport-Security response header, from any HTTPS server the victim contacts, leaves the victim's on-disk HSTS store at zero bytes. Every host in it loses HSTS protection, including hosts unrelated to the server that sent the header. curl exits 0 and prints nothing.

Two behaviours combine, each harmless alone. Curl_fopen() truncates the real store before it has produced any replacement content, and hsts_out() abandons the write when it meets an entry whose timestamp gmtime cannot render. The caller then unlinks the temporary file, so nothing is renamed over the already-emptied original.

The bug

One. lib/curl_fopen.c:98 opens the real target in "w" mode purely to fstat it and clone its permission bits onto a temporary file:

  *fh = curlx_fopen(filename, FOPEN_WRITETEXT);   /* "w", truncates */
  if(!*fh)
    goto fail;
  if(curlx_fstat(fileno(*fh), &sb) == -1 || !S_ISREG(sb.st_mode)) {
    return CURLE_OK;
  }
  curlx_fclose(*fh);

The store is empty on disk from this point, before a byte of output exists.

Two. lib/hsts.c:347-353, the serialiser gives up on an unrenderable timestamp:

static CURLcode hsts_out(struct stsentry *sts, FILE *fp)
{
  struct tm stamp;
  if(sts->expires != TIME_T_MAX) {
    CURLcode result = curlx_gmtime((time_t)sts->expires, &stamp);
    if(result)
      return result;                 /* abandons the write */

and lib/hsts.c:389-407, where one bad entry aborts the loop and discards the replacement:

      result = hsts_out(sts, out);
      if(result)
        break;
    ...
    if(result && tempstore)
      unlink(tempstore);             /* real file was already truncated */

The enabling step. lib/hsts.c:256-268 converts a parser overflow instead of rejecting it:

      rc = curlx_str_number(&vp, &expires, TIME_T_MAX);
      if(rc == STRE_OVERFLOW)
        expires = CURL_OFF_T_MAX;

So a stored expires above what gmtime accepts but below TIME_T_MAX is reachable from a header. Commit 4080ace060 rewrote this parser and preserved that conversion.

Reproduce

printf 'example.com "20340101 00:00:00"\n.bank.example "20340101 00:00:00"\n' > store.txt
# server replies to https:// with:
#   Strict-Transport-Security: max-age=4611686018427387904
curl --hsts store.txt https://attacker.example/
echo "exit=$?  size=$(stat -c %s store.txt)"
exit=0  size=0

Control, identical in every respect except the header value:

Strict-Transport-Security: max-age=31536000     ->  exit=0  size=256   intact

Downgrade, measured end to end after the wipe: the Switched from HTTP to HTTPS due to HSTS line is gone, curl dials port 80, and against a plaintext listener the query string ?token=SECRET123 left the machine in the clear.

The trigger window

max-age + now must land in (67768036191676799, TIME_T_MAX), exclusive of the upper endpoint. The lower edge is the largest time_t glibc gmtime_r accepts. The upper edge is fail-safe, because reaching CURL_OFF_T_MAX clamps to exactly TIME_T_MAX and routes into the literal "unlimited" branch at lib/hsts.c:336.

max-ageexitstore after
315360000256, intact
2534023008000257, intact
6776803619167680000, wiped
461168601842738790400, wiped
922337203506635736100, wiped
9223372036854775807 (TIME_T_MAX)0248, intact
999999999999999999999999990248, intact

Anything in [1e17, 9e18] works regardless of the victim's clock, so no clock knowledge is needed. It is still a window, and the naive "very large number" payload falls outside it. A 32-bit time_t is not required; this was measured on a 64-bit build.

Measured on x86_64 glibc, SIZEOF_TIME_T 8, HAVE_GMTIME_R 1.

Impact

Anyone who persists an HSTS store, which is the point of --hsts and CURLOPT_HSTSWRITEDATA. One header from a server the victim merely visits removes HSTS for every host in the store, so the header from attacker.example is what downgrades bank.example. Realising the loss needs a network position afterwards, which is why I am not asking for High.

The "Saving files" exclusion does not reach this: no attacker has write access to the directory. curl destroys its own store in response to a remote header, and the asset is a security control rather than a downloaded file.

Severity

Medium. CVE-2023-46219 ("HSTS long filename clears contents") is the same outcome and curl rated it Low, as it did CVE-2023-23915, CVE-2023-23914 and CVE-2024-9681. I am asking one level above CVE-2023-46219 on the trigger and not the outcome: that one needed an unusual local filename, this needs one response header from any server and it takes down unrelated hosts.

Fix

Either change breaks the chain; both is better.

  1. Stop truncating the real file to stat it, lib/curl_fopen.c:98. Use curlx_stat() on the path, or open O_RDONLY, and drop the curlx_fclose. Output already goes to the descriptor made at line 141. This fixes every Curl_fopen consumer at once, including alt-svc, and turns this whole family of failures from destructive into inert.
  2. Make hsts_out fail safe, lib/hsts.c:350. Skip the entry, or write it as "unlimited" the way the expires == TIME_T_MAX branch at line 337 already does. The loader already parses UNLIMITED, so no format change is needed.

Rejecting an unrenderable max-age at parse time would also work, but it patches this one trigger rather than the mechanism.

Impact

Impact

Anyone who persists an HSTS store, which is the point of --hsts and CURLOPT_HSTSWRITEDATA. One header from a server the victim merely visits removes HSTS for every host in the store, so the header from attacker.example is what downgrades bank.example. Realising the loss needs a network position afterwards, which is why I am not asking for High.

The "Saving files" exclusion does not reach this: no attacker has write access to the directory. curl destroys its own store in response to a remote header, and the asset is a security control rather than a downloaded file.

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