jf0x3f

$ cat writeup.md / 2026.11.01

When the address replaced the name: Apache Thrift C++ TLS identity confusion

A TLS certificate incorrectly connecting a requested network name to a resolved network address

THRIFT-6460 · CWE-297 · Apache Thrift C++ TSSLSocketFactory · fixed by PR #4055 · Medium severity

When a TLS client asks for api.example, the certificate must prove that the server is allowed to answer for api.example. Resolving that name to an IP address is only how the client finds the server; the address is not a replacement for the name the client intended to reach.

During a local source audit of Apache Thrift, I found that the C++ TLS path could blur that distinction. A factory-created client opened with a DNS name could accept a CA-valid certificate containing only the IP address reached by the socket. The certificate proved control of the destination address, but not the requested service name.

If an attacker could redirect the connection and present a trusted certificate for that destination IP, the client could establish TLS with the wrong RPC service. The encryption still worked—it was simply protecting a connection to the wrong identity.

Finding at a glance

PropertyValue
ComponentApache Thrift C++ TSSLSocketFactory, TSSLSocket::authorize(), and DefaultClientAccessManager
Confirmed revision06773440c2aa0f7134ebed9b7b736a2cb79f5d1a
WeaknessCWE-297: Improper Validation of Certificate with Host Mismatch
Broken invariantA DNS reference identity must be matched as a DNS name, not replaced by the address returned by name resolution.
Required conditionsFactory-created C++ TLS client, DNS reference, connection redirection, and a CA-valid certificate whose IP SAN covers the reached address but not the requested name.
ImpactImpersonation of the intended Thrift service, exposing the confidentiality and integrity of RPC traffic.
ResolutionPR #4055 / THRIFT-6460, merged as 3d15fac

The idea in plain language

Imagine asking a courier to deliver something to “the finance office.” A directory says that office is currently in Room 127. The courier reaches Room 127 and checks a badge that proves only “I am allowed to use Room 127.” That is not the same as proving “I am the finance office.”

TLS reference identities work the same way:

  • Reference identity: the name the application asked for, such as localhost or api.example.
  • Resolved address: the network location DNS returned, such as 127.0.0.1.
  • Presented identity: the names and addresses listed in the server certificate's Subject Alternative Name extension.

The verifier should compare like with like. A DNS reference is checked against DNS SAN entries. An IP-literal reference is checked against IP SAN entries. Name resolution must not silently change which identity the certificate is required to prove.

Where the trust decision went wrong

The affected path was narrower than “Thrift TLS does not verify hostnames.” C++ clients created by TSSLSocketFactory receive a DefaultClientAccessManager, and that manager is intended to perform the post-handshake identity check.

After OpenSSL validated the certificate chain, TSSLSocket::authorize() obtained the connected peer address with getpeername(). While walking the certificate's SAN entries, it offered DNS names to the manager as names—but offered every IP SAN against that connected peer address:

case GEN_DNS:
  decision = access_->verify(requestedHost, sanName, length);
  break;

case GEN_IPADD:
  decision = access_->verify(connectedPeer, sanAddress, length);
  break;

DefaultClientAccessManager returned ALLOW when the IP SAN matched the connected socket. That positive decision ended authorization before the verifier had established that the certificate covered the original DNS reference.

Client reference:     localhost          (DNS name)
DNS resolution:       127.0.0.1          (where to connect)
Certificate SAN:      IP:127.0.0.1       (address identity only)
Vulnerable decision:  ALLOW              (incorrect for a DNS reference)

Chain validation was not bypassed. The proof used a certificate signed by a synthetic local CA trusted only by the test client. The flaw was in deciding which peer identity that valid chain authenticated.

Local proof: three decisive certificates

The proof ran entirely on loopback with a synthetic CA, a pinned Apache Thrift revision, and no contact with a deployed service. It exercised the real C++ factory path rather than a reimplementation of the comparison logic.

Client referenceCertificate identityVulnerable resultMeaning
localhostDNS:localhostAcceptedExpected positive control
localhostUnrelated IP SANRejectedThe certificate still had to match the reached address
localhostIP:127.0.0.1 onlyAcceptedUnexpected: the resolved address replaced the DNS reference identity

The third case is the vulnerability. The client requested a DNS name, yet a certificate with no matching DNS identity was accepted because its IP SAN matched the socket's peer address.

Why this matters under the Thrift threat model

The project's threat model draws a useful line between a missing identity check and a broken identity check:

  • P8—certificate-chain validation: remained intact. The server certificate still had to chain to a configured trust anchor.
  • P9—server hostname verification: was violated. With an AccessManager present, the certificate was expected to cover the requested host.
  • Q52: confirms that factory-created C++ clients automatically receive DefaultClientAccessManager, making this the default factory path rather than an application forgetting to register a manager.
  • N7: treats direct TSSLSocket construction without an AccessManager as a documented non-finding. That exception did not apply here because the manager was present and returned the wrong answer.

This distinction is important. The report was not “hostname verification is optional.” It was “the enabled, default factory verifier accepts an identity that does not cover the requested DNS host.”

Credible attack path

Exploitation requires more than presenting any certificate. A realistic path has four parts:

  1. A C++ Thrift client uses TSSLSocketFactory to connect by DNS name.
  2. The attacker can redirect that connection through DNS manipulation, route control, a compromised network layer, or an equivalent mechanism.
  3. The attacker controls an endpoint at the reached address and can present a certificate chaining to a trust anchor accepted by the client.
  4. That certificate contains an IP SAN for the reached address but no DNS identity covering the name the client requested.

Under those conditions, the vulnerable client could accept the attacker's service. The attacker could then read RPC requests and responses, return modified data, or impersonate application behavior expected from the intended service. The issue did not grant a certificate or network position by itself; it turned those capabilities into a TLS identity bypass.

The fix: preserve the original identifier type

PR #4055 changes the client-side decision so IP SAN entries are consulted only when the host originally supplied to the socket is itself an address literal. A DNS name stays a DNS reference even after it resolves to an address.

The final patch classifies the original host with getaddrinfo(..., AI_NUMERICHOST). That matters because it follows the same address syntax the socket resolver accepts, including numeric forms that a narrower parser could miss.

Original client referenceCertificate identities considered
DNS name such as localhostDNS SAN entries, then the existing common-name fallback rules when applicable
IP literal such as 127.0.0.1IP SAN entries
Server mode or descriptor-only socketExisting behavior preserved

The merged patch also adds regression coverage in SecurityTest.cpp and updates the C++ TLS documentation. The D-language implementation was explicitly left outside this C++ fix.

Independent fix verification

After the patch merged as 3d15fac3b938177d1b7fe854804a2890a61ae964, I built that revision and reran the original local proof. The behavior changed exactly where it should:

  • DNS:localhost for a localhost reference remained accepted.
  • An unrelated IP SAN remained rejected.
  • IP:127.0.0.1 alone for a localhost reference was rejected with authorize: cannot authorize peer.
  • IP:127.0.0.1 for an actual 127.0.0.1 reference remained accepted.

That last control is as important as the rejection: the patch restores the DNS-versus-IP boundary without breaking legitimate clients that intentionally connect to an IP literal.

Defensive lessons

  • Keep the reference identity immutable. DNS resolution chooses a route; it must not rewrite the identity the certificate needs to prove.
  • Test identifier types as a matrix. Cover DNS reference/DNS SAN, DNS reference/IP SAN, IP reference/IP SAN, mismatches, and numeric-looking hostnames.
  • Separate chain trust from endpoint identity. A CA-valid certificate can still belong to the wrong service.
  • Verify the active path, not just helper functions. This finding depended on the interaction between the factory, socket, SAN loop, and default manager.
  • Preserve negative controls. A good fix must reject the confused identity while retaining legitimate DNS and IP-literal connections.

References