8000 Do not allow wildcards in the hostname for Valid Redirect Address (26.6) by rmartinc · Pull Request #49183 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
< 8000 tr class="js-expandable-line js-skip-tagsearch" data-position="">
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ protected KeycloakUriBuilder parseHierarchicalUri(String uri, Map<String,String>
if (scheme) this.scheme = match.get("scheme");
String authority = match.get("authority");
if (authority != null) {
this.authority = null;
this.authority = authority;
String host = authority;
int at = host.indexOf('@');
if (at > -1) {
Expand Down Expand Up @@ -747,6 +747,14 @@ public KeycloakUriBuilder replaceQueryParam(String name, Object... values) throw
return queryParam(name, values);
}

public String getSsp() {
return ssp;
}

public String getAuthority() {
return authority;
}

public String getHost() {
return host;
}
Expand Down
11 changes: 11 additions & 0 deletions docs/documentation/upgrading/topics/changes/changes-26_6_3.adoc
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
== Breaking changes

Breaking changes are identified as those that might require changes for existing users to their configurations or applications.
In minor or patch releases, {project_name} will only introduce breaking changes to fix bugs.

=== Valid redirect URIs for clients do not accept wildcards for hostname anymore

The wildcard comparison for valid redirect URIs does not affect the hostname anymore. Previously patterns like `$$https://example.com*$$` were accepted and lead to allow unintended domains like `$$https://example.com.other.domain.com$$`. Since this release, the hostname does not accept such type of wildcard and those patterns will be managed as if the wildcard is after the hostname `$$https://example.com/*$$`. The full wildcard for the hostname `$$https://*$$` is still accepted because its interpretation cannot be misunderstood.

Note that OAuth 2.0 recommends exact string matching in the link:https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-protecting-redirect-based-f[Security Best Current Practice] and draft for OAuth 2.1 enforces it. {project_name} recommends to not use any wildcard valid redirect URI for clients. See link:{adminguide_link}#unspecific-redirect-uris_server_administration_guide[Unspecific redirect URIs] in the {adminguide_name} for more information.

== Notable changes

Notable changes may include internal behavior changes that prevent common misconfigurations, bugs that are fixed, or changes to simplify running {project_name}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,22 +181,57 @@ private static String matchesRedirects(Set<String> validRedirects, String redire
if ("*".equals(validRedirect)) {
// the valid redirect * is a full wildcard for http(s) even if the redirect URI does not allow wildcards
return validRedirect;
} else if (validRedirect.endsWith("*") && !validRedirect.contains("?") && allowWildcards) {
// strip off the query or fragment components - we don't check them when wildcards are effective
int idx = redirect.indexOf('?');
if (idx == -1) {
idx = redirect.indexOf('#');
} else {
String validRedirectWildcard = allowWildcards ? checkValidRedirectWildcard(validRedirect) : null;
if (validRedirectWildcard != null) {
// strip off the query or fragment components - we don't check them when wildcards are effective
int idx = redirect.indexOf('?');
if (idx == -1) {
idx = redirect.indexOf('#');
}
String r = idx == -1 ? redirect : redirect.substring(0, idx);
// strip off *
int length = validRedirectWildcard.length() - 1;
validRedirectWildcard = validRedirectWildcard.substring(0, length);
if (r.startsWith(validRedirectWildcard)) {
return validRedirectWildcard;
}
// strip off trailing '/'
if (length - 1 > 0 && validRedirectWildcard.charAt(length - 1) == '/') {
length--;
}
validRedirectWildcard = validRedirectWildcard.substring(0, length);
if (validRedirectWildcard.equals(r)) {
return validRedirectWildcard;
}
} else if (validRedirect.equals(redirect)) {
return validRedirect;
}
String r = idx == -1 ? redirect : redirect.substring(0, idx);
// strip off *
int length = validRedirect.length() - 1;
validRedirect = validRedirect.substring(0, length);
if (r.startsWith(validRedirect)) return validRedirect;
// strip off trailing '/'
if (length - 1 > 0 && validRedirect.charAt(length - 1) == '/') length--;
validRedirect = validRedirect.substring(0, length);
if (validRedirect.equals(r)) return validRedirect;
} else if (validRedirect.equals(redirect)) return validRedirect;
}
}
return null;
}

private static String checkValidRedirectWildcard(String validRedirect) {
if (!validRedirect.endsWith("*") || validRedirect.contains("?") || validRedirect.contains("#")) {
return null; // no wildcard as before
}
KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(validRedirect, false);
if (uriBuilder.getPath() != null) {
return validRedirect; // wildcard valid on path
}
if (uriBuilder.getAuthority() != null) {
if (uriBuilder.getAuthority().equals("*") || uriBuilder.getAuthority().endsWith(":*")) {
return validRedirect; // on authority just full wildcard or on port
} else {
// treat the wildcard after the authority
validRedirect = validRedirect.substring(0, validRedirect.length() - 1);
validRedirect = validRedirect + "/*";
return validRedirect;
}
}
if (uriBuilder.getSsp() != null) {
return validRedirect; // wildcard valid on SSP
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,10 @@ public void testUserInfo() {
).collect(Collectors.toSet());

Assert.assertEquals("https://keycloak.org/index.html", RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/index.html", set, false));
Assert.assertEquals("https://test.com/index.html", RedirectUtils.verifyRedirectUri(session, null, "https://test.com/index.html", set, false));
Assert.assertEquals("https://test/index.html", RedirectUtils.verifyRedirectUri(session, null, "https://test/index.html", set, false));
Assert.assertEquals("https://something@keycloak.com/exact", RedirectUtils.verifyRedirectUri(session, null, "https://something@keycloak.com/exact", set, false));

Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://test.com/index.html", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://something@other.com/", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org@other.com", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org%2F@other.com", set, false));
Expand Down Expand Up @@ -284,4 +285,26 @@ public void testEncodedRedirectUri() {
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test%2Fanother/../any/path/", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://keycloak.org/test%2Fanother/%2E%2E/any/path/", set, false));
}

@Test
public void testWildcards() {
Set<String> set = Stream.of(
"https://test*",
"https://test.com:*",
"https://test.com/*",
"custom2:*",
"custom3://*"
).collect(Collectors.toSet());

Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://test.evil.com", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "https://testa.com", set, false));
Assert.assertNull(RedirectUtils.verifyRedirectUri(session, null, "custom3:/test", set, false));
Assert.assertEquals("https://test", RedirectUtils.verifyRedirectUri(session, null, "https://test", set, false));
Assert.assertEquals("https://test/something", RedirectUtils.verifyRedirectUri(session, null, "https://test/something", set, false));
Assert.assertEquals("https://test.com", RedirectUtils.verifyRedirectUri(session, null, "https://test.com", set, false));
Assert.assertEquals("https://test.com:4443/", RedirectUtils.verifyRedirectUri(session, null, "https://test.com:4443/", set, false));
Assert.assertEquals("custom2:custom", RedirectUtils.verifyRedirectUri(session, null, "custom2:custom", set, false));
Assert.assertEquals("custom3://custom", RedirectUtils.verifyRedirectUri(session, null, "custom3://custom", set, false));
Assert.assertEquals("https://test.com/lala", RedirectUtils.verifyRedirectUri(session, null, "https://test.com/lala", set, false));
}
}
Loading
0