urlapi: deny hostnames with more than one trailing dot

Or consisting of just a single dot.

Such names cannot be resolved with DNS.

While they *can* still be resolved with /etc/hosts or --resolve tricks,
they easily cause internal problems because their trailing dots.

Let's not allow them anymore.

Closes #21622
This commit is contained in:
Daniel Stenberg 2026-05-15 10:14:36 +02:00
parent 88bb7f885f
commit 9135294115
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
3 changed files with 23 additions and 12 deletions

View file

@ -475,6 +475,13 @@ static CURLUcode hostname_check(struct Curl_URL *u, char *hostname,
if(hlen != len)
/* hostname with bad content */
return CURLUE_BAD_HOSTNAME;
else if((hlen >= 2) &&
(hostname[hlen - 1] == '.') && (hostname[hlen - 2] == '.'))
/* more than one trailing dot is not allowed */
return CURLUE_BAD_HOSTNAME;
else if((hlen == 1) && (hostname[0] == '.'))
/* just a single dot is not allowed */
return CURLUE_BAD_HOSTNAME;
}
return CURLUE_OK;
}

View file

@ -127,7 +127,7 @@ class TestSSLUse:
# the SNI the server received is without trailing dot
assert r.json['SSL_TLS_SNI'] == env.domain1, f'{r.json}'
# use hostname with double trailing dot, verify handshake
# use hostname with double trailing dot
@pytest.mark.parametrize("proto", Env.http_protos())
def test_17_04_double_dot(self, env: Env, proto, httpd, nghttpx):
curl = CurlClient(env=env)
@ -142,10 +142,8 @@ class TestSSLUse:
if proto != 'h3': # we proxy h3
assert r.json['SSL_TLS_SNI'] == env.domain1, f'{r.json}'
assert False, f'should not have succeeded: {r.json}'
# 7 - Rustls rejects a servername with .. during setup
# 35 - LibreSSL rejects setting an SNI name with trailing dot
# 60 - peer name matching failed against certificate
assert r.exit_code in [7, 35, 60], f'{r}'
# 3 - not allowed in the URL
assert r.exit_code in [3], f'{r}'
# use ip address for connect
@pytest.mark.parametrize("proto", Env.http_protos())

View file

@ -196,20 +196,19 @@ static const struct testcase get_parts_list[] = {
"http://host:00080/",
"http | [11] | [12] | [13] | host | 80 | / | [16] | [17]",
0, 0, CURLUE_OK },
{ /* Single dot host - technically valid in some contexts but often
rejected */
{ /* Single dot host - not ok */
"http://./",
"http | [11] | [12] | [13] | . | [15] | / | [16] | [17]",
0, 0, CURLUE_OK },
"",
0, 0, CURLUE_BAD_HOSTNAME },
{ /* Host starting with a dash (RFC 1123 technically allows it, but many
parsers don't) */
"http://-atest/",
"http | [11] | [12] | [13] | -atest | [15] | / | [16] | [17]",
0, 0, CURLUE_OK },
{ /* Multiple trailing dots, not okay in DNS but works in /etc/hosts */
{ /* Multiple trailing dots is not okey */
"http://example.com../",
"http | [11] | [12] | [13] | example.com.. | [15] | / | [16] | [17]",
0, 0, CURLUE_OK },
"",
0, 0, CURLUE_BAD_HOSTNAME },
{ /* Empty IPv6 Zone ID */
"http://[fe80::1%]/",
"", 0, 0, CURLUE_BAD_IPV6 },
@ -626,6 +625,13 @@ static const struct testcase get_parts_list[] = {
};
static const struct urltestcase get_url_list[] = {
{"http://hej./", "http://hej./", 0, 0, CURLUE_OK},
{"http://hej../", "", 0, 0, CURLUE_BAD_HOSTNAME},
{"http://hej.../", "", 0, 0, CURLUE_BAD_HOSTNAME},
{"http://hej..../index.html", "", 0, 0, CURLUE_BAD_HOSTNAME},
{"http://.", "", 0, 0, CURLUE_BAD_HOSTNAME},
{"http://..", "", 0, 0, CURLUE_BAD_HOSTNAME},
{"http://...", "", 0, 0, CURLUE_BAD_HOSTNAME},
{"018.0.0.0", "http://018.0.0.0/", CURLU_GUESS_SCHEME, 0, CURLUE_OK},
{"08", "http://08/", CURLU_GUESS_SCHEME, 0, CURLUE_OK},
{"0", "http://0.0.0.0/", CURLU_GUESS_SCHEME, 0, CURLUE_OK},