HexBrain

curl · Report #4063328

curl tool: --hsts is ignored for a schemeless URL starting with a slash under --proto-default http

Informativestatus
lowseverity
13upvotes
8 Oct 2026disclosed

How it was found

Summary

When the curl tool gets --proto-default http and a URL without a scheme that starts with a slash, such as /example.com/path, it skips all the options set in http_setopts(): --hsts, --alt-svc, the cookie options, -L and the rest. libcurl still performs the transfer, over plain HTTP. Unless an earlier URL on the same command line has already loaded the --hsts file, a request to a host with a valid entry in that file goes out in plain text instead of being upgraded to HTTPS, and curl prints no warning.

Only the tool is affected, not libcurl.

Affected versions

curl 8.14.0 to 8.22.0, and master (755e36ead1).

It started with f97d372703 ("tool_operate: move config2setopts to separate file, split into subs", #17352), first released in 8.14.0 (2025-05-28). Its commit message lists "only set HTTP options if HTTP(S) is used": since then the tool sets the HTTP options, CURLOPT_HSTS included, only when it detects an http or https URL. Before it, CURLOPT_HSTS was set whenever --hsts was used.

I ran the reproducer below on 8.13.0 and on the parent of f97d372703 (9c8cbbda5b): request 3 is upgraded to HTTPS. On f97d372703, on every release from 8.14.0 to 8.22.0 and on master it is sent in plain text.

Steps to reproduce

cd "$(mktemp -d)"
printf 'hsts.example "20991231 23:59:59"\n' > hsts.txt
# plain-text HTTP server, needs port 8080 to be free
python3 -m http.server --bind 127.0.0.1 8080 > server.log 2>&1 &
SERVER=$!
sleep 1

# 1. Regular URL: upgraded to HTTPS by HSTS
curl -s -o /dev/null -w '%{url_effective} exit=%{exitcode}\n' \
  --hsts hsts.txt --resolve hsts.example:8080:127.0.0.1 \
  http://hsts.example:8080/a

# 2. Schemeless URL with --proto-default http: also upgraded
curl -s -o /dev/null -w '%{url_effective} exit=%{exitcode}\n' \
  --hsts hsts.txt --proto-default http \
  --resolve hsts.example:8080:127.0.0.1 hsts.example:8080/c

# 3. The same with a leading slash: not upgraded
curl -s -o /dev/null -w '%{url_effective} exit=%{exitcode}\n' \
  --hsts hsts.txt --proto-default http \
  --resolve hsts.example:8080:127.0.0.1 /hsts.example:8080/b

kill $SERVER
grep -ao '"GET [^"]*"' server.log

Output:

https://hsts.example:8080/a exit=35
https://hsts.example:8080/c exit=35
http://hsts.example:8080/b exit=0
"GET /b HTTP/1.1"

Requests 1 and 2 start a TLS handshake, which fails against the plain-text server (exit code 35 with the OpenSSL 3.6.4 builds I used). Request 3 is sent in plain text and the server logs it. With -v, requests 1 and 2 show "Switched from HTTP to HTTPS due to HSTS", request 3 does not.

Adding -L -b name=value -c jar.txt --alt-svc altsvc.txt --libcurl - to commands 2 and 3 shows the skipped options: the code generated for command 3 lacks CURLOPT_HSTS, CURLOPT_ALTSVC, CURLOPT_COOKIE, CURLOPT_COOKIEJAR, CURLOPT_FOLLOWLOCATION and CURLOPT_MAXREDIRS.

Root cause

Line numbers are for 755e36ead1.

url_proto_and_rewrite() in src/config2setopts.c parses the URL with curl_url_set() and CURLU_GUESS_SCHEME. For /hsts.example:8080/b that fails with CURLUE_NO_HOST, so the function returns the scheme "?" (line 201). http_setopts() then returns early because use_proto is neither proto_http nor proto_https (line 671), before it sets CURLOPT_HSTS (line 713).

libcurl prepends the default protocol to a URL that has no scheme (lib/url.c line 2091), which makes http:///hsts.example:8080/b. It accepts one to three slashes after the colon (docs/URL-SYNTAX.md), so the transfer goes to http://hsts.example:8080/b.

CVE-2026-12064 ("proto-default skips SSH verification") describes the same disconnect between the tool and libcurl for the SSH options. Its fix, ab3bb8cd8b ("config2setopts: use default protocol properly", #21983, in 8.21.0), applies --proto-default only after curl_url_set() succeeds, so it does not cover this URL. 8.21.0 and 8.22.0 are affected.

Conditions

  • Without --proto-default, curl rejects the URL because it has no host. With two or three leading slashes it is rejected too ("Unsupported number of slashes following scheme").
  • The transfers of one command line share an HSTS cache, so a leading-slash URL is upgraded if an earlier URL on the same command line has already loaded the --hsts file. With the options of command 3, http://hsts.example:8080/a /hsts.example:8080/b upgrades both requests, while /hsts.example:8080/b http://hsts.example:8080/a sends the first one in plain text.
  • libcurl is not affected: a program that sets CURLOPT_HSTS and CURLOPT_DEFAULT_PROTOCOL to "http" itself gets the upgrade for /hsts.example:8080/b (tested with master).

Impact

A request that --hsts should upgrade to HTTPS goes out in plain text, where a network attacker can read or modify it, and the user is not warned.

For the same URLs, -b cookies are not sent, -c does not write the jar and -L does not follow redirects. With --proto-default https the cookie and redirect options are skipped the same way. These are functional problems, not security ones.

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