| | |
| | | * limitations under the License. |
| | | */ |
| | | |
| | | package org.keycloak.protocol.oidc.utils; |
| | | package org.keycloak.protocol.cas.utils; |
| | | |
| | | import java.net.URI; |
| | | import java.util.Arrays; |
| | | import java.util.Collection; |
| | | import java.util.Collections; |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.Locale; |
| | | import java.util.Set; |
| | | import java.util.TreeSet; |
| | | import java.util.regex.Pattern; |
| | | |
| | | import org.keycloak.common.util.KeycloakUriBuilder; |
| | | import org.keycloak.common.util.MultivaluedHashMap; |
| | | import org.keycloak.common.util.UriUtils; |
| | | import org.keycloak.models.ClientModel; |
| | | import org.keycloak.models.Constants; |
| | | import org.keycloak.models.KeycloakSession; |
| | | import org.keycloak.models.KeycloakUriInfo; |
| | | import org.keycloak.models.RealmModel; |
| | | import org.keycloak.protocol.cas.CASLoginProtocol; |
| | | import org.keycloak.services.Urls; |
| | | import org.keycloak.services.util.ResolveRelative; |
| | | |
| | |
| | | |
| | | public static final Set<String> LOOPBACK_INTERFACES = new HashSet<>(Arrays.asList("localhost", "127.0.0.1", "[::1]")); |
| | | |
| | | private static final Set<String> FORBIDDEN_OIDC_PARAMS = Set.of( |
| | | CASLoginProtocol.SAMLART_RESPONSE_PARAM.toLowerCase(Locale.ROOT), |
| | | CASLoginProtocol.TICKET_RESPONSE_PARAM.toLowerCase(Locale.ROOT) |
| | | ); |
| | | |
| | | private static final Logger logger = Logger.getLogger(RedirectUtils.class); |
| | | |
| | | public static String verifyRedirectUri(KeycloakSession session, String redirectUri, ClientModel client) { |
| | |
| | | return null; |
| | | } |
| | | |
| | | // Check for HTTP Parameter Pollution - forbidden OIDC response parameters in redirect URI |
| | | if (containsForbiddenOidcParameters(originalRedirect)){ |
| | | return null; |
| | | } |
| | | |
| | | // check if the passed URI allows wildcards |
| | | boolean allowWildcards = areWildcardsAllowed(originalRedirect); |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | private static boolean containsForbiddenOidcParameters(URI originalRedirect) { |
| | | String query = originalRedirect.getRawQuery(); |
| | | if (query != null && !query.isEmpty()) { |
| | | MultivaluedHashMap<String, String> params =UriUtils.decodeQueryString(query); |
| | | for (String paramName : params.keySet()) { |
| | | if (FORBIDDEN_OIDC_PARAMS.contains(paramName.toLowerCase(Locale.ROOT))) { |
| | | logger.warnf("Redirect URI rejected: contains forbidden OIDC parameter '%s' in query string: scheme=%s, host=%s, path=%s", |
| | | paramName, |
| | | originalRedirect.getScheme(), |
| | | originalRedirect.getHost(), |
| | | originalRedirect.getPath()); |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private static URI toUri(String redirectUri) { |
| | | URI uri = null; |
| | | if (redirectUri != null) { |
| | |
| | | } |
| | | |
| | | // any access to parent folder /../ is unsafe with or without encoding |
| | | // <sep> = / | %2F | %5C | \ |
| | | // <dots> = "..", including %2E and %252E (double-encoded) variants |
| | | // <terminator> = / | %2F | %5C | \ | ; | %3B | %09 | %0A | %0D | %00 | end-of-input |
| | | private final static Pattern UNSAFE_PATH_PATTERN = Pattern.compile( |
| | | "(/|%2[fF]|%5[cC]|\\\\)(%2[eE]|\\.){2}(/|%2[fF]|%5[cC]|\\\\|;)|(/|%2[fF]|%5[cC]|\\\\)(%2[eE]|\\.){2}$"); |
| | | "(/|%2[fF]|%5[cC]|\\\\)(%2[eE]|%252[eE]|\\.){2}(/|%2[fF]|%5[cC]|\\\\|;|%3[bB]|%09|%0[aAdD]|%00|$)"); |
| | | |
| | | private static boolean areWildcardsAllowed(URI redirectUri) { |
| | | // wildcars are only allowed if no user-info and no unparsed authority and no unsafe pattern in path |
| | |
| | | } |
| | | return redirectUri; |
| | | } |
| | | |
| | | public static Set<String> resolveUrlsWithRedirects(KeycloakSession session, List<String> origUrls, |
| | | String rootUrl, List<String> redirectUris, boolean returnAsOrigins) { |
| | | |
| | | Set<String> refactoredUrls = (origUrls != null) ? new HashSet<>(origUrls) : new HashSet<>(); |
| | | if (refactoredUrls.contains(Constants.INCLUDE_REDIRECTS)) { |
| | | refactoredUrls.remove(Constants.INCLUDE_REDIRECTS); |
| | | |
| | | Set<String> redirectsToProcess = (redirectUris != null) ? new HashSet<>(redirectUris) : Collections.emptySet(); |
| | | for (String redirectUri : resolveValidRedirects(session, rootUrl, redirectsToProcess)) { |
| | | if (isValidScheme(redirectUri)) { |
| | | if (returnAsOrigins) { |
| | | refactoredUrls.add(UriUtils.getOrigin(redirectUri)); |
| | | } else { |
| | | refactoredUrls.add(redirectUri); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return refactoredUrls; |
| | | } |
| | | |
| | | private static boolean isValidScheme(String url) { |
| | | return url != null && (url.startsWith("http://") || url.startsWith("https://")); |
| | | } |
| | | } |