Handle canonical hostname checks for localhost on Windows (#42799)

Closes: #42794

Signed-off-by: Peter Zaoral <pepo48@gmail.com>
This commit is contained in:
Peter Zaoral 2025-10-17 15:40:08 +02:00 committed by GitHub
parent 4dc398354a
commit 2300b3fc78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 44 additions and 4 deletions

View File

@ -0,0 +1,9 @@
// Release notes should contain only headline-worthy new features,
// assuming that people who migrate will read the upgrading guide anyway.
//
== Breaking Fix for Windows in Loopback Hostname Verification
This release introduces a breaking change for Windows users: setups that previously relied on custom machine names or non-standard hostnames for loopback (e.g., `127.0.0.1` resolving to a custom name) may require updates to their trusted domain configuration. Only `localhost` and `*.localhost` are now recognized for loopback verification.
Keycloak now consistently normalizes loopback addresses to `localhost` for domain verification across all platforms. This change ensures predictable behavior for trusted domain checks, regardless of the underlying OS.

View File

@ -175,14 +175,22 @@ public class TrustedHostClientRegistrationPolicy implements ClientRegistrationPo
logger.debugf("Trying verify request from address '%s' of host '%s' by domains", hostAddress, hostname);
// On Windows, reverse lookup for loopback may return the IP (e.g., 127.0.0.1) instead of 'localhost'. Normalize to 'localhost' for consistent domain checks.
if (hostname.equals(address.getHostAddress()) && address.isLoopbackAddress()) {
hostname = "localhost";
}
if (hostname.equals(address.getHostAddress())) {
logger.debugf("The hostAddress '%s' was not resolved to a hostname", hostAddress);
return null;
}
if (Arrays.stream(InetAddress.getAllByName(hostname)).filter(a -> address.equals(a)).findAny().isEmpty()) {
logger.debugf("The hostAddress '%s' is not among the direct lookups returned resolving '%s'", hostAddress, hostname);
return null;
// For loopback, skip strict forward-confirm check after normalizing to 'localhost' to avoid platform differences.
if (!address.isLoopbackAddress()) {
if (Arrays.stream(InetAddress.getAllByName(hostname)).filter(a -> address.equals(a)).findAny().isEmpty()) {
logger.debugf("The hostAddress '%s' is not among the direct lookups returned resolving '%s'", hostAddress, hostname);
return null;
}
}
for (String confDomain : trustedDomains) {
@ -192,7 +200,17 @@ public class TrustedHostClientRegistrationPolicy implements ClientRegistrationPo
}
}
} catch (UnknownHostException uhe) {
logger.debugf(uhe, "Request of address '%s' came from unknown host. Skip verification by domains", hostAddress);
logger.debugf(uhe, "Request of address '%s' came from unknown host. Skip verification by domains unless it's within localhost domain", hostAddress);
String lower = hostAddress == null ? null : hostAddress.toLowerCase();
if (lower != null && ("localhost".equals(lower) || lower.endsWith(".localhost"))) {
for (String confDomain : trustedDomains) {
if (checkTrustedDomain(lower, confDomain)) {
logger.debugf("Treating host '%s' as loopback due to localhost domain and returning success by trusted domain '%s'", lower, confDomain);
return lower;
}
}
}
}
return null;

View File

@ -102,6 +102,19 @@ public class TrustedHostClientRegistrationPolicyTest {
policy.getTrustedHosts(), policy.getTrustedDomains()));
}
@Test
public void testLocalhostDomainFallback() {
TrustedHostClientRegistrationPolicyFactory factory = new TrustedHostClientRegistrationPolicyFactory();
ComponentModel model = createComponentModel("*.localhost");
TrustedHostClientRegistrationPolicy policy = (TrustedHostClientRegistrationPolicy) factory.create(session, model);
// Simulate a hostname that would fail DNS resolution on some platforms
// but matches the trusted domain fallback logic
assertTrue(policy.verifyHost("other.localhost"));
assertTrue(policy.verifyHost("localhost"));
assertFalse(policy.verifyHost("otherlocalhost"));
}
private ComponentModel createComponentModel(String... hosts) {
ComponentModel model = new ComponentModel();
model.put(TrustedHostClientRegistrationPolicyFactory.HOST_SENDING_REGISTRATION_REQUEST_MUST_MATCH, "true");