Skip to content

Core: Expose HostnameVerificationPolicy in TLSConfigurer#15500

Open
adutra wants to merge 7 commits intoapache:mainfrom
adutra:http-client-hostname-verification-policy
Open

Core: Expose HostnameVerificationPolicy in TLSConfigurer#15500
adutra wants to merge 7 commits intoapache:mainfrom
adutra:http-client-hostname-verification-policy

Conversation

@adutra
Copy link
Copy Markdown
Contributor

@adutra adutra commented Mar 3, 2026

Fixes #15598.

This change exposes HostnameVerificationPolicy in TLSConfigurer. The default policy is set to CLIENT.

I also added a unit test that intentionally uses a custom hostname verifier that is incompatible with JSSE hostname verification strategies. Thus, the test cannot pass unless the verification policy is set to CLIENT.

@adutra
Copy link
Copy Markdown
Contributor Author

adutra commented Mar 3, 2026

FYI @bryanck

@adutra adutra force-pushed the http-client-hostname-verification-policy branch from ad082ad to 2858de7 Compare March 3, 2026 11:17
@adutra adutra changed the title Expose HostnameVerificationPolicy in TLSConfigurer Core: Expose HostnameVerificationPolicy in TLSConfigurer Mar 3, 2026
@singhpk234
Copy link
Copy Markdown
Contributor

This component is particularly useful when attempting to bypass hostname verification, e.g. by using the NoopHostnameVerifier.

Alex, why would we allow such a verification to be bypassed, do you have some real world use case for this ?

awssdk-s3accessgrants = { module = "software.amazon.s3.accessgrants:aws-s3-accessgrants-java-plugin", version.ref = "awssdk-s3accessgrants" }
azuresdk-bom = { module = "com.azure:azure-sdk-bom", version.ref = "azuresdk-bom" }
bson = { module = "org.mongodb:bson", version.ref = "bson-ver"}
bouncycastle-bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bouncycastle" }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is new dependency, do we have to add the licence of this dependency to one of the LICENSE files in Iceberg?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, since it's used only in tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for clarifying.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's good because bouncycastle complicates export rules, or at least due diligence

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assumes the bouncycastle bcpkix is a transitive dep already pulled in by the mockerserver lib. is the purpose here just to upgrade the version?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good question! Short answer: yes.

MockServer does bring BouncyCastle transitively — bcprov, bcpkix, and bcutil – but at version 1.72, which is somewhat old.

The problem is that other deps (Hadoop) bring bcprov 1.82, and Gradle resolves the conflict by upgrading bcprov to 1.82. But bcpkix and bcutil stay at 1.72, since nothing else declares a higher version. This creates a mismatch: bcpkix 1.72 + bcprov 1.82 = NoClassDefFoundError.

I only upgraded bcpkix here, because that is enough for running the TLS tests. If you prefer, I can also explicitly upgrade bcutil.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is probably good to just ensure the same version for bouncycastle family of jars. That is the current practice. E.g., there is just one version number for the two jetty jars in the toml file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right: I implemented your suggestion.

@adutra
Copy link
Copy Markdown
Contributor Author

adutra commented Mar 4, 2026

Alex, why would we allow such a verification to be bypassed, do you have some real world use case for this ?

You could find yourself in the situation where the catalog server has TLS enabled, and its certificate shows a SAN of e.g. catalog.bigcorp.com; but if the client/engine is in the same cluster/network, it could actually be contacting the catalog through its internal IP instead, e.g. https://1.2.3.4:8181/api/catalog. In that case, the hostname verification will fail.

(Please note: I mentioned NoopHostnameVerifier just as an example of possible usage of HostnameVerificationPolicy.)

@adutra
Copy link
Copy Markdown
Contributor Author

adutra commented Mar 4, 2026

And I forgot to mention: NoopHostnameVerifier is also quite useful in tests :-)

@adutra
Copy link
Copy Markdown
Contributor Author

adutra commented Mar 8, 2026

@bryanck since you introduced TLSConfigurer: are you OK with the changes in this PR?

adutra added 2 commits March 10, 2026 18:45
Apache HttpClient 5.4 introduced a new component: `HostnameVerificationPolicy`, which determines whether hostname verification is done by the JSSE provider (at socket level, during TLS handshake), the HttpClient (after TLS handshake), or both.

This change exposes `HostnameVerificationPolicy` in `TLSConfigurer`. This component is particularly useful when attempting to bypass hostname verification, e.g. by using the `NoopHostnameVerifier`. The default policy is set to `BOTH`, which produces the same result as before.
@adutra adutra force-pushed the http-client-hostname-verification-policy branch from 2858de7 to 6e3cfb8 Compare March 10, 2026 17:46
@adutra
Copy link
Copy Markdown
Contributor Author

adutra commented Mar 10, 2026

Update: I was able to find the exact change that caused this regression:

In 1.10 we use httpclient5 version 5.5, where the DefaultClientTlsStrategy constructor used by Iceberg is as follows:

    public DefaultClientTlsStrategy(
            final SSLContext sslContext,
            final String[] supportedProtocols,
            final String[] supportedCipherSuites,
            final SSLBufferMode sslBufferManagement,
            final HostnameVerifier hostnameVerifier) {
        this(sslContext, supportedProtocols, supportedCipherSuites, sslBufferManagement, HostnameVerificationPolicy.CLIENT, hostnameVerifier);
    }

https://github.com/apache/httpcomponents-client/blob/c5bd9af6a47af3f2683209f0b818f1cf109026f6/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/DefaultClientTlsStrategy.java#L124-L131

But in 1.11 we upgraded httpclient5 to version 5.6, where the same constructor becomes:

    public DefaultClientTlsStrategy(
            final SSLContext sslContext,
            final String[] supportedProtocols,
            final String[] supportedCipherSuites,
            final SSLBufferMode sslBufferManagement,
            final HostnameVerifier hostnameVerifier) {
        this(sslContext, supportedProtocols, supportedCipherSuites, sslBufferManagement, null, hostnameVerifier);
    }

https://github.com/apache/httpcomponents-client/blob/cee67d86809aa23577968f9e7e7bf922a9892512/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/DefaultClientTlsStrategy.java#L127

Passing null instead of HostnameVerificationPolicy.CLIENT is not the same when there is a non-null hostnameVerifier:

https://github.com/apache/httpcomponents-client/blob/master/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/AbstractClientTlsStrategy.java#L101

So, this is imho clearly a regression, and in fact the default value for TLSConfigurer.hostnameVerificationPolicy() should be CLIENT, not BOTH if we want to restore the 1.10 behavior:

default HostnameVerificationPolicy hostnameVerificationPolicy() {
    return HostnameVerificationPolicy.CLIENT;
  }

I will change that.

@singhpk234 could you please add this to the 1.11 milestone? Now I'm really convinced it's a regression.

@danielcweeks danielcweeks self-requested a review March 11, 2026 16:25
@singhpk234 singhpk234 self-requested a review March 11, 2026 16:27
@adutra
Copy link
Copy Markdown
Contributor Author

adutra commented Mar 12, 2026

@danielcweeks and @singhpk234 : as requested, I created an issue to better explain why I think we have a regression and how this PR fixes it: #15598. PTAL 🙏

@stevenzwu
Copy link
Copy Markdown
Contributor

You could find yourself in the situation where the catalog server has TLS enabled, and its certificate shows a SAN of e.g. catalog.bigcorp.com; but if the client/engine is in the same cluster/network, it could actually be contacting the catalog through its internal IP instead, e.g. https://1.2.3.4:8181/api/catalog. In that case, the hostname verification will fail.

(Please note: I mentioned NoopHostnameVerifier just as an example of possible usage of HostnameVerificationPolicy.)

I am wondering about the default value of HostnameVerificationPolicy.CLIENT. I know the purpose is maintain the same behavior. Unit test can configure NoopHostnameVerifier to bypass the check or mapping from ip to the hostname.

Is HostnameVerificationPolicy.BOTH the safer config for prod env and used as the default? if prod env also wants IP address to work, the safer practice is to add the IP address to the Subject Alternative Name (SAN) .

@stevenzwu stevenzwu added this to the Iceberg 1.11.0 milestone Mar 20, 2026
exclude group: 'junit'
}
testImplementation libs.awaitility
testImplementation libs.bouncycastle.bcpkix
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's worth at least adding a comment on why these dependencies are being added, since it's not obvious

@steveloughran
Copy link
Copy Markdown
Contributor

Is HostnameVerificationPolicy.BOTH the safer config for prod env and used as the default?

Best to lock down and require relaxation in deployments than to unlock and regret it.

More formally CWE-1188: Initialization of a Resource with an Insecure Default

@adutra
Copy link
Copy Markdown
Contributor Author

adutra commented Mar 20, 2026

I am wondering about the default value of HostnameVerificationPolicy.CLIENT.

BOTH is safer, but also introduces a behavioral change compared to 1.10. Are we OK with that?

@stevenzwu
Copy link
Copy Markdown
Contributor

I am wondering about the default value of HostnameVerificationPolicy.CLIENT.

BOTH is safer, but also introduces a behavioral change compared to 1.10. Are we OK with that?

Can you send an email to dev@ to get broader feedback? It might be ok introduce a slight behavior change. Most production use cases probably should use DNS anyway. For IP address usage, users can add the IP address to the cert SAN.

This is the only question I have for this PR. Exposing hostnameVerificationPolicy in the TLS configurer makes sense. Unit test can configure it to noop to use the loopback address.

@aihuaxu
Copy link
Copy Markdown
Contributor

aihuaxu commented Mar 24, 2026

@adutra @stevenzwu Want to follow up if this is required for 1.11.0 release. Can you help driving this? Thanks.

@stevenzwu
Copy link
Copy Markdown
Contributor

@aihuaxu this is needed for 1.11.0, as the original change was added in the 1.11 scope. Waiting for the dev thread to get more tractions.

return SSLContexts.createDefault();
}

default HostnameVerificationPolicy hostnameVerificationPolicy() {
Copy link
Copy Markdown
Member

@RussellSpitzer RussellSpitzer Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote on the ML list a bit about this, but do we really need this to be customizable?

It seems to be that there are only really 2 options that make sense.

Either
hostnameVerifier is replaced and we should use "client" mode
or
hostnameVerifier is not replaced and we should be using "builtin" mode

return HostnameVerificationPolicy.CLIENT;
}

default HostnameVerifier hostnameVerifier() {
Copy link
Copy Markdown
Contributor

@stevenzwu stevenzwu Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RussellSpitzer @adutra this is the method where I suggested adding Javadoc. or you think this is redundant and the intention is obvious from the API already?

/**
 * If overwritten, only the custom verifier is executed and
 * Java Secure Socket Extension (JSSE) built-in verifier won't be executed.
 */

I also like Russell's suggestion from email of making this method returning null so that we can get rid of the new method above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stevenzwu @RussellSpitzer I implemented the suggested approach, PTAL.

I want to stress though that returning null instead of HttpsSupport.getDefaultHostnameVerifier() here is a behavioral change.

The built-in verifier and Apache's default verifier both do roughly the same verifications, but the latter is more strict/complete. They also kick in in different phases and throw different exceptions.

Copy link
Copy Markdown
Contributor

@stevenzwu stevenzwu Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to stress though that returning null instead of HttpsSupport.getDefaultHostnameVerifier() here is a behavioral change.

Not really a behavior change btw 1.10 and 1.11 since the original change from Bryant hasn't been released it.

And returning null is essentially the same as using the default built-in hostname verifier.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really a behavior change btw 1.10 and 1.11 since the original change from Bryant hasn't been released it.

From what I see, TLSConfigurer has been released in 1.10.

Copy link
Copy Markdown
Contributor

@stevenzwu stevenzwu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

just a nit comment on the test code.

assertThatThrownBy(() -> builtInVerifierClient.head(path, Map.of(), (unused) -> {}))
.rootCause()
.isInstanceOf(CertificateException.class)
.hasMessage("No subject alternative names matching IP address 127.0.0.1 found");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently this is wording JDK specific, May come up later if folks are testing on other architectures. But I think we are mostly testing on OpenJDK, Oracle JDK and the other OpenJDK based versions.

OpenJDK / Oracle JDK: "No subject alternative names matching IP address 127.0.0.1 found" — this is the message in sun.security.util.HostnameChecker.

IBM Semeru / J9: IBM's JSSE implementation has its own HostnameChecker equivalent with different wording. Historically it uses messages like "No name matching 127.0.0.1 found" or similar variations.

Eclipse Temurin / Azul Zulu: These are typically OpenJDK-based and share the message, but there's no guarantee across versions — the sun.security package is internal and its strings can change between releases without notice.

Copy link
Copy Markdown
Member

@RussellSpitzer RussellSpitzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, left a few nits on style

@RussellSpitzer
Copy link
Copy Markdown
Member

@steveloughran - @nastra - Any remaining comments on this one?

@adutra adutra force-pushed the http-client-hostname-verification-policy branch from 1a8f7d3 to e631a0f Compare April 1, 2026 15:58
@adutra adutra force-pushed the http-client-hostname-verification-policy branch from e631a0f to b5bb63a Compare April 1, 2026 16:14
@kevinjqliu
Copy link
Copy Markdown
Contributor

kevinjqliu commented Apr 1, 2026

original PR commit:
b3adeb1
part of 1.10.0 and 1.10.1

but as Alex mentioned in the community sync, the library update was NOT part of 1.10.x

This issue requires both the PR and the library update

Copy link
Copy Markdown
Contributor

@singhpk234 singhpk234 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to know when we a choosing JSSE host name verifier, how much can we now influence with java ops / system prop and what are the new cases we are opening to when are shifting from lib to this one.

@RussellSpitzer
Copy link
Copy Markdown
Member

It would be nice to know when we a choosing JSSE host name verifier, how much can we now influence with java ops / system prop and what are the new cases we are opening to when are shifting from lib to this one.

What are you looking for in particular?

I tried to do some Cursor assisted analysis and as far as I can tell neither of the verifiers are actually changeable via system properties or java opts. Neither of the OpenJDK HostnameChecker verifier nor the Apache DefaultHostnameVerifier use system.getProperty.

Cursor did list a few things that would indirectly change behaviors but they seem to be all a result of the fact that JSSE will will do hostname verification during the TLS handshake rather than the DefaultHostnameVerifier which checks after the handshake.

@singhpk234
Copy link
Copy Markdown
Contributor

singhpk234 commented Apr 5, 2026

Thanks for analysis @RussellSpitzer, I was mostly curious about how jvm flags can influence host verification now, when JSSE is being used one such flag jdk.tls.trustNameService

switching the default verification engine from Apache's library to JSSE exposes a knob that was previously irrelevant. specially what if JVM was running with this flag on will now come into effect in rest client
But

  • It defaults to false
  • It's static final, set at JVM boot (not exploitable in runtime)
  • Anyone setting it to true has already made a conscious decision to trust their DNS
  • The attack requires controlling both the JVM launch flags AND a DNS server - at that point one have bigger problems

It was not meant to be a blocker of this pr, but more from curiously purpose and i think we should proceed if there is a concencus on the approach.

Though one thing I think while researching this flag, i would recommend we mention this in JSSE as default in the release notes explictly!

@adutra
Copy link
Copy Markdown
Contributor Author

adutra commented Apr 7, 2026

one such flag jdk.tls.trustNameService

This property lives in SSLSocketImpl.useImplicitHost() and only triggers when an SSLSocket is connected without an explicit hostname. I believe Apache HC5 always provide the target hostname explicitly from the URL when setting up connections (see AbstractClientTlsStrategy.upgrade()). So the incriminated code path is never reached in practice.

The attack requires controlling both the JVM launch flags AND a DNS server - at that point one have bigger problems

Exactly. If an attacker can set JVM launch flags, they can already disable TLS entirely, swap trust stores, etc. The game is already over.

@adutra
Copy link
Copy Markdown
Contributor Author

adutra commented Apr 7, 2026

Though one thing I think while researching this flag, i would recommend we mention this in JSSE as default in the release notes explictly!

That's a good idea! Here is a suggestion for the release notes:

When no custom HostnameVerifier is provided via TLSConfigurer, the REST client now uses JSSE's built-in hostname verification (during the TLS handshake) instead of Apache HttpClient's DefaultHostnameVerifier (after the handshake). Both verify the server certificate's SANs/CN against the target hostname. Users providing a custom HostnameVerifier are unaffected — their verifier continues to be used exclusively.

cc @aihuaxu

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTTPClient: regression in TLS hostname verification

9 participants