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
| Property | Value |
|---|---|
| Component | Apache Thrift C++ TSSLSocketFactory, TSSLSocket::authorize(), and DefaultClientAccessManager |
| Confirmed revision | 06773440c2aa0f7134ebed9b7b736a2cb79f5d1a |
| Weakness | CWE-297: Improper Validation of Certificate with Host Mismatch |
| Broken invariant | A DNS reference identity must be matched as a DNS name, not replaced by the address returned by name resolution. |
| Required conditions | Factory-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. |
| Impact | Impersonation of the intended Thrift service, exposing the confidentiality and integrity of RPC traffic. |
| Resolution | PR #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
localhostorapi.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 reference | Certificate identity | Vulnerable result | Meaning |
|---|---|---|---|
localhost | DNS:localhost | Accepted | Expected positive control |
localhost | Unrelated IP SAN | Rejected | The certificate still had to match the reached address |
localhost | IP:127.0.0.1 only | Accepted | Unexpected: 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
TSSLSocketconstruction 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:
- A C++ Thrift client uses
TSSLSocketFactoryto connect by DNS name. - The attacker can redirect that connection through DNS manipulation, route control, a compromised network layer, or an equivalent mechanism.
- The attacker controls an endpoint at the reached address and can present a certificate chaining to a trust anchor accepted by the client.
- 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 reference | Certificate identities considered |
|---|---|
DNS name such as localhost | DNS SAN entries, then the existing common-name fallback rules when applicable |
IP literal such as 127.0.0.1 | IP SAN entries |
| Server mode or descriptor-only socket | Existing 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:localhostfor alocalhostreference remained accepted.- An unrelated IP SAN remained rejected.
IP:127.0.0.1alone for alocalhostreference was rejected withauthorize: cannot authorize peer.IP:127.0.0.1for an actual127.0.0.1reference 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.