curl · Report #3791191
Trailing-Dot Hostname in Redirect Silently Strips Client Certificate and Auth Credentials
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
- 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
- Start an HTTPS server that requires client certs and redirects
/page1tohttps://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()
- 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"
- 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.