HexBrain

curl · Report #3791191

Trailing-Dot Hostname in Redirect Silently Strips Client Certificate and Auth Credentials

Informativestatus
mediumseverity
12upvotes
10 Jun 2026disclosed

How it was found

Summary

When curl follows a redirect where the Location header contains a hostname with a trailing dot (e.g., https://example.com./path), Curl_peer_equal() in peer.c:321-330 compares the original hostname (example.com) against the redirect target (example.com.) using curl_strequal, which does not normalize trailing dots. Since example.com ≠ example.com., the function incorrectly determines the origins differ. This false mismatch causes Curl_ssl_easy_config_complete() (vtls_config.c:280-307) to clear all client certificate fields (clientcert, cert_blob, key, key_blob, key_passwd, cert_type), and Curl_auth_allowed_to_origin() (vauth.c:148-149) to suppress all HTTP auth credentials (Basic, Digest, NTLM, Negotiate, Bearer). The redirected request is sent in an unauthenticated state. Per RFC, example.com and example.com. are semantically identical — the trailing dot represents the DNS root.

This is an internal inconsistency: hsts.c:156-158 correctly strips trailing dots before comparison, but peer_same_hostname does not. The Curl_peer_equal check is the implementation of the security boundary documented in CREDENTIALS.md: "Credential information from CURLOPT_* settings is only applicable for the initial origin. Any followup request going to another origin must not use it." The boundary logic is correct; it is triggered incorrectly because peer_same_hostname fails to recognize the same origin.

PoC (requires a server that redirects with trailing dot):

CURL *easy = curl_easy_init();
curl_easy_setopt(easy, CURLOPT_URL, "https://localhost:18443/page1");
curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(easy, CURLOPT_SSLCERT, "client.pem");
curl_easy_setopt(easy, CURLOPT_SSLKEY, "client.key");
curl_easy_setopt(easy, CURLOPT_CAINFO, "ca.pem");
// Server returns: Location: https://localhost.:18443/page2
// Observed: second TLS handshake has no CERT verify → empty certificate sent
curl_easy_perform(easy);

Affected version

curl 8.21.0-DEV (x86_64-pc-linux-gnu) libcurl/8.21.0-DEV OpenSSL/3.0.13

All versions with Curl_peer_equal / peer_same_hostname (introduced with the peer comparison refactor) are affected. All 6 TLS backends, all platforms.

Steps To Reproduce

  1. Generate CA, server, and client certificates for mTLS:
# CA
openssl req -x509 -newkey rsa:2048 -keyout ca.key -out ca.pem -days 365 -nodes -subj "/CN=TestCA"
# Server cert (SAN: DNS:localhost, DNS:localhost.)
openssl req -newkey rsa:2048 -keyout server.key -out server.csr -nodes -subj "/CN=localhost"
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out server.pem -days 365 -extfile <(echo "subjectAltName=DNS:localhost,DNS:localhost.")
# Client cert
openssl req -newkey rsa:2048 -keyout client.key -out client.csr -nodes -subj "/CN=client"
openssl x509 -req -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out client.pem -days 365
  1. Start an HTTPS server that requires client certs and redirects /page1 to https://localhost.:PORT/page2 (trailing dot):
# Simplified — see verification/finding004_mock_required.py for full code
class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/page1':
            self.send_response(302)
            self.send_header('Location', f'https://localhost.:{PORT}/page2')
            self.end_headers()
        else:
            self.send_response(200)
            self.end_headers()
  1. Run the test program (or curl CLI):
# CLI:
curl -v --cacert ca.pem --cert client.pem --key client.key -L https://localhost:PORT/page1

# Observe:
# - First TLS handshake: "CERT verify (15) OUT" → client cert sent ✓
# - Second TLS handshake: no "CERT verify" → empty cert, server rejects ✗
#   "tlsv13 alert certificate required"
  1. Control test (normal redirect without trailing dot) — both connections succeed with client cert.

Impact

Client certificate authentication bypass: In mTLS environments, a malicious server can send a redirect with a trailing dot appended to its own hostname. curl silently strips the client certificate on the redirected connection. In strict mTLS deployments this causes a DoS (server rejects). In deployments where client auth is optional but grants different privilege levels, the request succeeds with reduced privileges without the user's knowledge.

HTTP auth credential suppression: The same Curl_peer_equal mismatch also suppresses all HTTP authentication types (Basic, Digest, NTLM, Negotiate, Bearer), TLS-SRP, and proxy auth on the redirected request. A trailing-dot redirect causes Authorization headers to be dropped even though the origin is unchanged.

This follows the same security boundary as CVE-2022-27774 (redirect credential handling). CVE-2022-27774 was credential leakage to a different origin; this is credential stripping from the same origin misidentified as different.

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