diff --git a/docs/libcurl/curl_easy_duphandle.md b/docs/libcurl/curl_easy_duphandle.md
index 5ea9c4fba0..0de33496ca 100644
--- a/docs/libcurl/curl_easy_duphandle.md
+++ b/docs/libcurl/curl_easy_duphandle.md
@@ -42,7 +42,9 @@ SSL sessions and no cookies. It also does not inherit any share object states
or options (created as if CURLOPT_SHARE(3) was set to NULL).
If the source handle has HSTS or alt-svc enabled, the duplicate gets data read
-data from the main filename to populate the cache.
+from the main filename to populate the cache. For HSTS, any entries learned at
+runtime (E.g. `Strict-Transport-Security` response headers) are also copied to
+the duplicate handle.
In multi-threaded programs, this function must be called in a synchronous way,
the input handle may not be in use when cloned.
diff --git a/lib/easy.c b/lib/easy.c
index 8d61241952..d60bdaed7b 100644
--- a/lib/easy.c
+++ b/lib/easy.c
@@ -1052,6 +1052,11 @@ 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 entries learned at runtime. (E.g. 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 a8e6bcae43..0884ef5893 100644
--- a/lib/hsts.c
+++ b/lib/hsts.c
@@ -130,6 +130,25 @@ 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 entries learned at runtime. E.g. Strict-Transport-Security.
+ */
+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 result = hsts_create(dst, sts->host, strlen(sts->host),
+ sts->includeSubDomains != 0, sts->expires);
+ if(result)
+ return result;
+ }
+ }
+ 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..08215f5eaa 100644
--- a/lib/hsts.h
+++ b/lib/hsts.h
@@ -54,6 +54,7 @@ struct hsts {
struct hsts *Curl_hsts_init(void);
void Curl_hsts_cleanup(struct hsts **hp);
+CURLcode Curl_hsts_copy(struct hsts *dst, struct hsts *src);
CURLcode Curl_hsts_parse(struct hsts *h, const char *hostname,
const char *header);
CURLcode Curl_hsts_save(struct Curl_easy *data, struct hsts *h,
diff --git a/tests/data/Makefile.am b/tests/data/Makefile.am
index 8c693f4e7f..dfb238345d 100644
--- a/tests/data/Makefile.am
+++ b/tests/data/Makefile.am
@@ -231,7 +231,7 @@ test1800 test1801 test1802 test1847 test1848 test1849 test1850 test1851 \
\
test1900 test1901 test1902 test1903 test1904 test1905 test1906 test1907 \
test1908 test1909 test1910 test1911 test1912 test1913 test1914 test1915 \
-test1916 test1917 test1918 test1919 test1920 test1921 \
+test1916 test1917 test1918 test1919 test1920 test1921 test1922 \
\
test1933 test1934 test1935 test1936 test1937 test1938 test1939 test1940 \
test1941 test1942 test1943 test1944 test1945 test1946 test1947 test1948 \
diff --git a/tests/data/test1922 b/tests/data/test1922
new file mode 100644
index 0000000000..dcf30557ff
--- /dev/null
+++ b/tests/data/test1922
@@ -0,0 +1,91 @@
+
+
+
+
+HTTP
+HTTP proxy
+HSTS
+curl_easy_duphandle
+
+
+
+
+
+
+HTTP/1.1 200 OK
+Date: Tue, 09 Nov 2010 14:49:00 GMT
+Server: test-server/fake
+Content-Type: text/plain
+Content-Length: 5
+Strict-Transport-Security: max-age=31536000
+
+Hello
+
+
+
+
+HTTP/1.1 403 Forbidden
+Content-Length: 0
+Connection: close
+
+
+
+
+
+
+HSTS
+https
+Debug
+proxy
+
+
+http
+http-proxy
+
+
+CURL_HSTS_HTTP=yes
+
+
+curl_easy_duphandle copies HSTS cache, auto upgrading HTTP to HTTPS.
+
+
+lib%TESTNUMBER
+
+
+- %HOSTIP %HTTPPORT %PROXYPORT
+
+
+
+
+# First request: original handle GETs from the http server; the response
+# carries Strict-Transport-Security, populating the live HSTS cache that
+# the dup inherits.
+
+GET /%TESTNUMBER HTTP/1.1
+Host: hsts.example.com:%HTTPPORT
+Accept: */*
+
+
+# Second request: dup handle upgraded HTTP to HTTPS by copied HSTS cache,
+# proxy receives CONNECT to port 443 proving the upgrade happened
+
+CONNECT hsts.example.com:443 HTTP/1.1
+Host: hsts.example.com:443
+Proxy-Connection: Keep-Alive
+
+
+
+First request: HTTPS cache populated
+Dup effective URL: https://hsts.example.com/%TESTNUMBER
+
+# CURLE_COULDNT_CONNECT (7) is intentional: The proxy rejects the CONNECT
+# to port 443, collapsing the tunnel. All that is being validated is the
+# CONNECT to port 443 itself.
+
+7
+
+
+
diff --git a/tests/libtest/Makefile.inc b/tests/libtest/Makefile.inc
index d9a94a1e71..22fff2272f 100644
--- a/tests/libtest/Makefile.inc
+++ b/tests/libtest/Makefile.inc
@@ -104,6 +104,7 @@ TESTS_C = \
lib1900.c lib1901.c lib1902.c lib1903.c lib1905.c lib1906.c lib1907.c \
lib1908.c lib1910.c lib1911.c lib1912.c lib1913.c \
lib1915.c lib1916.c lib1918.c lib1919.c lib1920.c lib1921.c \
+ lib1922.c \
lib1933.c lib1934.c lib1935.c lib1936.c lib1937.c lib1938.c lib1939.c \
lib1940.c lib1945.c \
lib1947.c lib1948.c \
diff --git a/tests/libtest/lib1922.c b/tests/libtest/lib1922.c
new file mode 100644
index 0000000000..fffb6d4fcc
--- /dev/null
+++ b/tests/libtest/lib1922.c
@@ -0,0 +1,123 @@
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, , et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+#include "first.h"
+
+static size_t test_lib1922_discard_write(char *ptr, size_t size, size_t nmemb,
+ void *ud)
+{
+ (void)ptr; (void)ud;
+ return size * nmemb;
+}
+
+static CURLcode test_lib1922(const char *URL)
+{
+ CURLcode result = CURLE_OK;
+ CURL *curl = NULL;
+ CURL *dup = NULL;
+ struct curl_slist *resolve = NULL;
+ char resolve_entry[256];
+ char direct_url[256];
+ char http_url[256];
+ char proxy_url[256];
+ const char *effective = NULL;
+ const char *host = libtest_arg2; /* %HOSTIP */
+ const char *httpport = libtest_arg3; /* %HTTPPORT */
+ const char *proxyport = libtest_arg4;/* %PROXYPORT */
+
+ (void)URL;
+
+ if(!host || !httpport || !proxyport) {
+ curl_mfprintf(stderr,
+ "Usage: lib1922 - \n");
+ return TEST_ERR_MAJOR_BAD;
+ }
+
+ /* Synthetic DNS so hsts.example.com resolves to the test server. */
+ curl_msnprintf(resolve_entry, sizeof(resolve_entry),
+ "hsts.example.com:%s:%s", httpport, host);
+ resolve = curl_slist_append(NULL, resolve_entry);
+ if(!resolve) {
+ return CURLE_OUT_OF_MEMORY;
+ }
+
+ curl_msnprintf(direct_url, sizeof(direct_url),
+ "http://hsts.example.com:%s/%d", httpport, 1922);
+ curl_msnprintf(http_url, sizeof(http_url),
+ "http://hsts.example.com/%d", 1922);
+ curl_msnprintf(proxy_url, sizeof(proxy_url),
+ "http://%s:%s", host, proxyport);
+
+ global_init(CURL_GLOBAL_ALL);
+ easy_init(curl);
+
+ easy_setopt(curl, CURLOPT_WRITEFUNCTION, test_lib1922_discard_write);
+ easy_setopt(curl, CURLOPT_RESOLVE, resolve);
+ easy_setopt(curl, CURLOPT_URL, direct_url);
+ easy_setopt(curl, CURLOPT_HSTS_CTRL, CURLHSTS_ENABLE);
+
+ /* Direct HTTP request: Server returns Strict-Transport-Security.
+ * CURL_HSTS_HTTP env var (set in the test) allows processing it over
+ * HTTP in debug builds, populating the live HSTS cache. */
+ result = curl_easy_perform(curl);
+ if(result) {
+ curl_mfprintf(stderr, "First perform failed: %d (%s)\n",
+ result, curl_easy_strerror(result));
+ goto test_cleanup;
+ }
+ curl_mprintf("First request: HTTPS cache populated\n");
+
+ dup = curl_easy_duphandle(curl);
+ if(!dup) {
+ result = CURLE_FAILED_INIT;
+ goto test_cleanup;
+ }
+
+ /* Point the dup at the plain HTTP URL for the same hostname, via a proxy.
+ * The copied HSTS cache upgrades the URL to HTTPS, causing a CONNECT to
+ * port 443. The test proxy rejects CONNECT with 403, so curl returns
+ * CURLE_COULDNT_CONNECT (7). The CONNECT to port 443 is itself the proof
+ * of the upgrade. */
+ easy_setopt(dup, CURLOPT_URL, http_url);
+ easy_setopt(dup, CURLOPT_PROXY, proxy_url);
+
+ result = curl_easy_perform(dup);
+ if(result != CURLE_COULDNT_CONNECT) {
+ curl_mfprintf(stderr, "Dup perform unexpected result: %d (%s)\n",
+ result, curl_easy_strerror(result));
+ goto test_cleanup;
+ }
+
+ /* Confirm the dup's URL was upgraded to HTTPS by the copied HSTS cache. */
+ curl_easy_getinfo(dup, CURLINFO_EFFECTIVE_URL, &effective);
+ if(effective) {
+ curl_mprintf("Dup effective URL: %s\n", effective);
+ }
+
+test_cleanup:
+ curl_easy_cleanup(curl);
+ curl_easy_cleanup(dup);
+ curl_slist_free_all(resolve);
+ curl_global_cleanup();
+ return result;
+}