fix: returning addresses instead of hosts on the ClientConnection (#35247)
also consolidates checks of whether a host or address is local closes: #35216 closes: #34671 Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
parent
198214310e
commit
0eb0281bf2
8 changed files with 98 additions and 40 deletions
|
|
@ -25,6 +25,9 @@ package org.keycloak.common;
|
|||
*/
|
||||
public interface ClientConnection {
|
||||
|
||||
/**
|
||||
* @return the address as a string if it is available, otherwise null
|
||||
*/
|
||||
String getRemoteAddr();
|
||||
String getRemoteHost();
|
||||
int getRemotePort();
|
||||
|
|
|
|||
|
|
@ -35,23 +35,29 @@ public enum SslRequired {
|
|||
return isRequired(connection.getRemoteAddr());
|
||||
}
|
||||
|
||||
public boolean isRequired(String address) {
|
||||
public boolean isRequired(String host) {
|
||||
switch (this) {
|
||||
case ALL:
|
||||
return true;
|
||||
case NONE:
|
||||
return false;
|
||||
case EXTERNAL:
|
||||
return !isLocal(address);
|
||||
// NOTE: this is sometimes using hostnames here, which require DNS resolution
|
||||
// It assumes that the resolution will be the same on the client side
|
||||
// - this will go away once EXTERNAL is no longer supported
|
||||
return !isLocal(host);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLocal(String remoteAddress) {
|
||||
private boolean isLocal(String host) {
|
||||
if (host == null || host.isEmpty()) {
|
||||
return false; // InetAddress.getByName returns localhost for these
|
||||
}
|
||||
try {
|
||||
InetAddress inetAddress = InetAddress.getByName(remoteAddress);
|
||||
return inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isSiteLocalAddress() || inetAddress.isLinkLocalAddress() || isUniqueLocal(inetAddress);
|
||||
InetAddress inetAddress = InetAddress.getByName(host);
|
||||
return inetAddress.isLoopbackAddress() || inetAddress.isSiteLocalAddress() || inetAddress.isLinkLocalAddress() || isUniqueLocal(inetAddress);
|
||||
} catch (UnknownHostException e) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package org.keycloak.common.enums;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class SslRequiredTest {
|
||||
|
||||
@Test
|
||||
public void sslRequiredExternalTest() throws IOException {
|
||||
assertFalse(SslRequired.EXTERNAL.isRequired("127.0.0.1"));
|
||||
assertTrue(SslRequired.EXTERNAL.isRequired((String)null));
|
||||
assertTrue(SslRequired.EXTERNAL.isRequired(""));
|
||||
assertTrue(SslRequired.EXTERNAL.isRequired("0.0.0.0"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ public final class QuarkusClientConnection implements ClientConnection {
|
|||
|
||||
@Override
|
||||
public String getRemoteAddr() {
|
||||
return request.remoteAddress().host();
|
||||
return request.remoteAddress().hostAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -46,7 +46,7 @@ public final class QuarkusClientConnection implements ClientConnection {
|
|||
|
||||
@Override
|
||||
public String getLocalAddr() {
|
||||
return request.localAddress().host();
|
||||
return request.localAddress().hostAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
package org.keycloak.it.cli.dist;
|
||||
|
||||
import io.quarkus.test.junit.main.Launch;
|
||||
import io.quarkus.test.junit.main.LaunchResult;
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.config.RedirectConfig;
|
||||
import io.restassured.config.RestAssuredConfig;
|
||||
|
|
@ -41,6 +42,9 @@ import static org.hamcrest.Matchers.containsString;
|
|||
@RawDistOnly(reason = "Containers are immutable")
|
||||
public class ProxyHostnameV2DistTest {
|
||||
|
||||
private static final String ADDRESS = "12.23.45.67";
|
||||
private static final String NOT_ADDRESS = "notaddress";
|
||||
|
||||
@BeforeAll
|
||||
public static void onBeforeAll() {
|
||||
RestAssured.useRelaxedHTTPSValidation();
|
||||
|
|
@ -78,10 +82,14 @@ public class ProxyHostnameV2DistTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@Launch({ "start-dev", "--hostname-strict=false", "--proxy-headers=forwarded" })
|
||||
public void testForwardedProxyHeaders() {
|
||||
@Launch({ "start-dev", "--hostname-strict=false", "--proxy-headers=forwarded", "--spi-event-listener-provider=jboss-logging" })
|
||||
public void testForwardedProxyHeaders(LaunchResult result) {
|
||||
assertForwardedHeader();
|
||||
assertXForwardedHeadersAreIgnored();
|
||||
|
||||
CLIResult cliResult = (CLIResult)result;
|
||||
cliResult.assertNoMessage(NOT_ADDRESS);
|
||||
cliResult.assertMessage(ADDRESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -92,13 +100,15 @@ public class ProxyHostnameV2DistTest {
|
|||
}
|
||||
|
||||
private void assertForwardedHeader() {
|
||||
assertForwardedHeader("https://test:1234/admin");
|
||||
// trigger a login error
|
||||
assertForwardedHeader("http://mykeycloak.org:8080/realms/master/protocol/openid-connect/auth?client_id=security-admin-console", "https://test:1234/admin", ADDRESS);
|
||||
assertForwardedHeader("http://mykeycloak.org:8080/realms/master/protocol/openid-connect/auth?client_id=security-admin-console", "https://test:1234/admin", NOT_ADDRESS);
|
||||
}
|
||||
|
||||
private void assertForwardedHeader(String expectedUrl) {
|
||||
private void assertForwardedHeader(String url, String expectedUrl, String forAddress) {
|
||||
given()
|
||||
.header("Forwarded", "for=12.34.56.78;host=test:1234;proto=https, for=23.45.67.89")
|
||||
.when().get("http://mykeycloak.org:8080")
|
||||
.header("Forwarded", "for="+forAddress+";host=test:1234;proto=https, for=23.45.67.89")
|
||||
.when().get(url)
|
||||
.then().header(HttpHeaders.LOCATION, containsString(expectedUrl));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,13 +51,12 @@ import org.keycloak.theme.Theme;
|
|||
import org.keycloak.theme.freemarker.FreeMarkerProvider;
|
||||
import org.keycloak.urls.UrlType;
|
||||
import org.keycloak.utils.MediaType;
|
||||
import org.keycloak.utils.SecureContextResolver;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
|
@ -271,25 +270,17 @@ public class WelcomeResource {
|
|||
}
|
||||
|
||||
private boolean isLocal() {
|
||||
try {
|
||||
ClientConnection clientConnection = session.getContext().getConnection();
|
||||
InetAddress remoteInetAddress = InetAddress.getByName(clientConnection.getRemoteAddr());
|
||||
InetAddress localInetAddress = InetAddress.getByName(clientConnection.getLocalAddr());
|
||||
HttpRequest request = session.getContext().getHttpRequest();
|
||||
HttpHeaders headers = request.getHttpHeaders();
|
||||
String xForwardedFor = headers.getHeaderString("X-Forwarded-For");
|
||||
logger.debugf("Checking WelcomePage. Remote address: %s, Local address: %s, X-Forwarded-For header: %s", remoteInetAddress.toString(), localInetAddress.toString(), xForwardedFor);
|
||||
ClientConnection clientConnection = session.getContext().getConnection();
|
||||
String remoteAddress = clientConnection.getRemoteAddr();
|
||||
String localAddress = clientConnection.getLocalAddr();
|
||||
HttpRequest request = session.getContext().getHttpRequest();
|
||||
HttpHeaders headers = request.getHttpHeaders();
|
||||
String xForwardedFor = headers.getHeaderString("X-Forwarded-For");
|
||||
String forwarded = headers.getHeaderString("Forwarded");
|
||||
logger.debugf("Checking WelcomePage. Remote address: %s, Local address: %s, X-Forwarded-For header: %s, Forwarded header: %s", remoteAddress.toString(), localAddress.toString(), xForwardedFor, forwarded);
|
||||
|
||||
// Access through AJP protocol (loadbalancer) may cause that remoteAddress is "127.0.0.1".
|
||||
// So consider that welcome page accessed locally just if it was accessed really through "localhost" URL and without loadbalancer (x-forwarded-for header is empty).
|
||||
return isLocalAddress(remoteInetAddress) && isLocalAddress(localInetAddress) && xForwardedFor == null;
|
||||
} catch (UnknownHostException e) {
|
||||
throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLocalAddress(InetAddress inetAddress) {
|
||||
return inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress();
|
||||
// Consider that welcome page accessed locally just if it was accessed really through "localhost" URL and without loadbalancer (x-forwarded-for and forwarded header is empty).
|
||||
return xForwardedFor == null && forwarded == null && SecureContextResolver.isLocalAddress(remoteAddress) && SecureContextResolver.isLocalAddress(localAddress);
|
||||
}
|
||||
|
||||
private String setCsrfCookie() {
|
||||
|
|
|
|||
|
|
@ -57,13 +57,7 @@ public class SecureContextResolver {
|
|||
return false;
|
||||
}
|
||||
|
||||
// The host matches a CIDR notation of ::1/128
|
||||
if (host.equals("[::1]") || host.equals("[0000:0000:0000:0000:0000:0000:0000:0001]")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The host matches a CIDR notation of 127.0.0.0/8
|
||||
if (LOCALHOST_IPV4.matcher(host).matches()) {
|
||||
if (isLocalAddress(host)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -73,4 +67,26 @@ public class SecureContextResolver {
|
|||
|
||||
return host.endsWith(".localhost") || host.endsWith(".localhost.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the given address is the localhost
|
||||
* @param address
|
||||
* @return false if the address is not localhost or not an address value
|
||||
*/
|
||||
public static boolean isLocalAddress(String address) {
|
||||
if (address == null) {
|
||||
return false;
|
||||
}
|
||||
// The host matches a CIDR notation of ::1/128
|
||||
if (address.equals("[::1]") || address.equals("[0000:0000:0000:0000:0000:0000:0000:0001]")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The host matches a CIDR notation of 127.0.0.0/8
|
||||
if (LOCALHOST_IPV4.matcher(address).matches()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import org.junit.Assert;
|
|||
import org.junit.Test;
|
||||
import org.keycloak.representations.account.DeviceRepresentation;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.function.Supplier;
|
||||
|
|
@ -47,6 +50,7 @@ public class SecureContextResolverTest {
|
|||
assertSecureContext("http://[::2]", false);
|
||||
assertSecureContext("http://[2001:0000:130F:0000:0000:09C0:876A:130B]", false);
|
||||
assertSecureContext("http://::1", false);
|
||||
assertSecureContext("http://[FE80:0000:130F:0000:0000:09C0:876A:130B]", false);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -59,6 +63,14 @@ public class SecureContextResolverTest {
|
|||
assertSecureContext("http://test.localhostn", false);
|
||||
assertSecureContext("http://test.localhost.not", false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsLocalhost() {
|
||||
assertTrue(SecureContextResolver.isLocalAddress("127.0.0.1"));
|
||||
assertFalse(SecureContextResolver.isLocalAddress("not.an.ip"));
|
||||
assertFalse(SecureContextResolver.isLocalAddress(null));
|
||||
assertFalse(SecureContextResolver.isLocalAddress(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQuirksSafari() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue