HACKERONE

curl: Low priority HSTS bypass in curl_easy_duphandle()_H1:3769293

Description

## Summary:

curl_easy_duphandle() creates a fresh HSTS store for the cloned handle and populates it from the configured files and callbacks, but never copies entries acquired from Strict-Transport-Security response headers during the parent's lifetime. This means the client using a cloned handle may accidentally send secrets to servers in plain text when it was already instructed not to do so.

## Affected version

Commit: efc3f2309e1c87c67700350f7df8da508cd307cd
Date: 2026-05-26.

Tested on WSL2. clang-19 w/ ASAN and GNU ldd. I have a test bench attached you can run. It uses Python to call a C extension that uses socketpair() and direct C calls to talk to libcurl.

## Steps To Reproduce:

(I am really hoping the patch is super obvious.)

Untar the attached `cyber.tgz` in the top level of curl (` tar xzf cyber.tgz`) so it creates `curl/cyber`. `cd` into `curl/cyber` and run `build.sh`. Then run ` ./run.sh lib_easy.c_duphandle_hsts.py`

With the patch I get:

```
...
* Switched from HTTP to HTTPS due to HSTS => https://x.test/secret
* Alt-svc check wanted=1, allowed=1
* check Alt-Svc for host 'x.test'
...
* SSL Trust: peer verification disabled
* TLS connect error: error:00000000:lib(0)::reason(0)
* OpenSSL SSL_connect: Broken pipe in connection to x.test:443
* closing connection #0
...
```

Without the patch I get:

```
...
Established connection to x.test (127.0.0.1 port 80) from port 0
* using HTTP/1.x
* sending last upload chunk of 115 bytes
* Curl_xfer_send(len=115, eos=1) -> 0, 115
> GET /secret HTTP/1.1
Host: x.test
Accept: */*
TE: gzip
Accept-Encoding: deflate, gzip, zstd
Connection: TE

* Request completely sent off
< HTTP/1.1 200 OK
< Content-Length: 58
< Connection: close
< Content-Type: text/plain
* shutting down connection #0...
...
```

## Patch:

```c
From 4de23f2cdf034fd7d405fd262367fb792ccec6be: Mon Sep 17 00:00:00 2001
From: Adrian Johnston <[email protected]>
Date: Fri, 29 May 2026 00:00:15 -0700
Subject: [PATCH] hsts_bypass_easy_duphandle

---
lib/easy.c | 3 +++
lib/hsts.c | 18 ++++++++++++++++++
lib/hsts.h | 1 +
3 files changed, 22 insertions(+)

diff --git a/lib/easy.c b/lib/easy.c
index a472d6ddc5..10f814d093 100644
--- a/lib/easy.c
+++ b/lib/easy.c
@@ -1052,6 +1052,9 @@ CURL *curl_easy_duphandle(CURL *curl)
(void)Curl_hsts_loadfile(outcurl,
outcurl->hsts, outcurl->set.str[STRING_HSTS]);
(void)Curl_hsts_loadcb(outcurl, outcurl->hsts);
+ /* copy runtime-learned entries (from Strict-Transport-Security headers) */
+ if(Curl_hsts_copy(outcurl->hsts, data->hsts))
+ goto fail;
}
#endif

diff --git a/lib/hsts.c b/lib/hsts.c
index 94738874b5..a5275200c6 100644
--- a/lib/hsts.c
+++ b/lib/hsts.c
@@ -130,6 +130,24 @@ static CURLcode hsts_create(struct hsts *h,
return CURLE_OK;
}

+/* Copy all live entries from src into dst. Used by curl_easy_duphandle so the
+ * clone inherits runtime-learned STS entries, not just file/callback ones. */
+CURLcode Curl_hsts_copy(struct hsts *dst, struct hsts *src)
+{
+ struct Curl_llist_node *e;
+ time_t now = time(NULL);
+ for(e = Curl_llist_head(&src->list); e; e = Curl_node_next(e)) {
+ struct stsentry *sts = Curl_node_elem(e);
+ if(sts->expires > now) {
+ CURLcode rc = hsts_create(dst, sts->host, strlen(sts->host),
+ sts->includeSubDomains, sts->expires);
+ if(rc)
+ return rc;
+ }
+ }
+ return CURLE_OK;
+}
+
/*
* Return the matching HSTS entry, or NULL if the given hostname is not
* currently an HSTS one.
diff --git a/lib/hsts.h b/lib/hsts.h
index d4c7fe826b..1ff1667759 100644
--- a/lib/hsts.h
+++ b/lib/hsts.h
@@ -65,6 +65,7 @@ CURLcode Curl_hsts_loadcb(struct Curl_easy *data,
CURLcode Curl_hsts_loadfiles(struct Curl_easy *data);

bool Curl_hsts_applies(struct hsts *h, const struct Curl_peer *dest);
+CURLcode Curl_hsts_copy(struct hsts *dst, struct hsts *src);

#else
#define Curl_hsts_cleanup(x)
--
2.51.0
```

## Impact

## Summary:

This is not an "attack vector." It just introduces the risk that clients of libcurl using it in a particular manner may accidentally send secrets like passwords or API keys in plain text when the server has instructed them not to.

I spent a lot of time working on this and reviewed and tested the information I am sending you carefully. If you have an issue with me using AI I understand. I find it is a productivity multiplier even if I have to spend 1/2 my time teaching it how to be a senior engineer over and over.
Visit Original Source

Basic Information

ID H1:3769293
Published May 29, 2026 at 09:18
Modified Jun 1, 2026 at 00:17

πŸ’­ Join the Security Discussion

πŸ”’ Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.