fix: refining v2 hostname validation (#32659)

closes: #32643

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
Steven Hawkins 2024-09-06 11:49:25 -04:00 committed by GitHub
parent 50f53bbbba
commit 58d742bb5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 95 additions and 19 deletions

View file

@ -17,6 +17,10 @@
package org.keycloak.url;
import java.net.URI;
import java.util.Arrays;
import java.util.Optional;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.models.KeycloakSession;
@ -24,24 +28,16 @@ import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.urls.HostnameProvider;
import org.keycloak.urls.HostnameProviderFactory;
import java.net.URI;
import java.util.Optional;
import java.util.regex.Pattern;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class HostnameV2ProviderFactory implements HostnameProviderFactory, EnvironmentDependentProviderFactory {
private static final String INVALID_HOSTNAME = "Provided hostname is neither a plain hostname nor a valid URL";
private String hostname;
private URI hostnameUrl;
private URI adminUrl;
private Boolean backchannelDynamic;
// Simplified regexes for hostname validations; further validations are performed when instantiating URI object
private static final String hostnameStringPattern = "[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*";
private static final Pattern hostnamePattern = Pattern.compile("^" + hostnameStringPattern + "$");
private static final Pattern hostnameUrlPattern = Pattern.compile("^(http|https)://" + hostnameStringPattern + "(:\\d+)?(/[\\w-]+)*/?$");
@Override
public void init(Config.Scope config) {
// Strict mode is used just for enforcing that hostname is set
@ -57,11 +53,10 @@ public class HostnameV2ProviderFactory implements HostnameProviderFactory, Envir
// Set hostname, can be either a full URL, or just hostname
if (hostnameRaw != null) {
if (hostnamePattern.matcher(hostnameRaw).matches()) {
hostname = hostnameRaw;
}
else {
hostnameUrl = validateAndCreateUri(hostnameRaw, "Provided hostname is neither a plain hostname or a valid URL");
if (!(hostnameRaw.startsWith("http://") || hostnameRaw.startsWith("https://"))) {
validateAndSetHostname(hostnameRaw);
} else {
hostnameUrl = validateAndCreateUri(hostnameRaw, INVALID_HOSTNAME);
}
}
@ -78,17 +73,36 @@ public class HostnameV2ProviderFactory implements HostnameProviderFactory, Envir
throw new IllegalArgumentException("hostname-backchannel-dynamic must be set to false if hostname is not provided as full URL");
}
}
private void validateAndSetHostname(String hostname) {
URI result;
try {
result = URI.create("http://"+hostname);
}
catch (IllegalArgumentException e) {
throw new IllegalArgumentException(INVALID_HOSTNAME, e);
}
if (result.getHost() == null || !result.getHost().equals(hostname)) {
throw new IllegalArgumentException(INVALID_HOSTNAME);
}
this.hostname = hostname;
}
private URI validateAndCreateUri(String uri, String validationFailedMessage) {
if (!hostnameUrlPattern.matcher(uri).matches()) {
throw new IllegalArgumentException(validationFailedMessage);
}
URI result;
try {
return URI.create(uri);
result = URI.create(uri);
}
catch (IllegalArgumentException e) {
throw new IllegalArgumentException(validationFailedMessage, e);
}
if (!Arrays.asList("http", "https").contains(result.getScheme())) {
throw new IllegalArgumentException(validationFailedMessage);
}
if (result.getRawUserInfo() != null || result.getRawQuery() != null || result.getRawFragment() != null) {
throw new IllegalArgumentException(validationFailedMessage);
}
return result;
}
@Override

View file

@ -0,0 +1,62 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.url;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import org.keycloak.utils.ScopeUtil;
public class HostnameV2ProviderFactoryTest {
@Test
public void hostnameUrlValidationTest() throws IOException{
assertHostname("https://my-example.com/auth.this", true);
assertHostname("https://my-example.com:8080", true);
assertHostname("https://my-example.com/auth%20this", true);
assertHostname("my-example.com", true);
assertHostname("192.196.0.0", true);
assertHostname("[2001:0000:130F:0000:0000:09C0:876A:130B]", true);
assertHostname("https://my-example.com?auth.this", false);
assertHostname("my-example.com/auth.this", false);
assertHostname("https://my-example.com:8080#fragment", false);
assertHostname("my-example.com:8080", false);
assertHostname("ldap://my-example.com/auth%20this", false);
assertHostname("?my-example.com", false);
assertHostname("192.196.0.5555", false);
}
private void assertHostname(String hostname, boolean valid) {
Map<String, String> values = new HashMap<>();
values.put("hostname", hostname);
HostnameV2ProviderFactory factory = new HostnameV2ProviderFactory();
try {
factory.init(ScopeUtil.createScope(values));
assertTrue(valid);
} catch (IllegalArgumentException e) {
assertFalse(valid);
}
}
}

View file

@ -180,7 +180,7 @@ public class HostnameV2Test extends AbstractKeycloakTest {
@Test
public void testInvalidHostnameUrl() {
testStartupFailure("Provided hostname is neither a plain hostname or a valid URL",
testStartupFailure("Provided hostname is neither a plain hostname nor a valid URL",
"htt://127.0.0.1.nip.io", null, null, true);
}