hsts: duplicate live HSTS data in curl_easy_duphandle

Verified by test 1922

Closes #21809
This commit is contained in:
A Johnston 2026-06-01 14:52:23 -07:00 committed by Daniel Stenberg
parent 4ead4285a6
commit 084ceb6601
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
8 changed files with 244 additions and 2 deletions

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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,

View file

@ -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 \

91
tests/data/test1922 Normal file
View file

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
HTTP proxy
HSTS
curl_easy_duphandle
</keywords>
</info>
<reply>
<!-- Response to the original handle's direct HTTP request.
Strict-Transport-Security header populates the live HSTS cache. -->
<data nocheck="yes" crlf="headers">
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
</data>
<!-- Response to the dup handle's proxy CONNECT attempt.
The CONNECT to port 443 proves the copied
HSTS cache upgraded the dup's HTTP URL to HTTPS. -->
<connect crlf="headers">
HTTP/1.1 403 Forbidden
Content-Length: 0
Connection: close
</connect>
</reply>
<client>
<features>
HSTS
https
Debug
proxy
</features>
<server>
http
http-proxy
</server>
<setenv>
CURL_HSTS_HTTP=yes
</setenv>
<name>
curl_easy_duphandle copies HSTS cache, auto upgrading HTTP to HTTPS.
</name>
<tool>
lib%TESTNUMBER
</tool>
<command>
- %HOSTIP %HTTPPORT %PROXYPORT
</command>
</client>
<verify>
# First request: original handle GETs from the http server; the response
# carries Strict-Transport-Security, populating the live HSTS cache that
# the dup inherits.
<protocol crlf="headers">
GET /%TESTNUMBER HTTP/1.1
Host: hsts.example.com:%HTTPPORT
Accept: */*
</protocol>
# Second request: dup handle upgraded HTTP to HTTPS by copied HSTS cache,
# proxy receives CONNECT to port 443 proving the upgrade happened
<proxy crlf="headers">
CONNECT hsts.example.com:443 HTTP/1.1
Host: hsts.example.com:443
Proxy-Connection: Keep-Alive
</proxy>
<stdout>
First request: HTTPS cache populated
Dup effective URL: https://hsts.example.com/%TESTNUMBER
</stdout>
# 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.
<errorcode>
7
</errorcode>
</verify>
</testcase>

View file

@ -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 \

123
tests/libtest/lib1922.c Normal file
View file

@ -0,0 +1,123 @@
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, 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 - <host> <httpport> <proxyport>\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;
}