| | |
| | | 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.OAuth2Constants; |
| | | 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; |
| | |
| | | |
| | | 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( |
| | | OAuth2Constants.CODE, |
| | | OAuth2Constants.ID_TOKEN, |
| | | OAuth2Constants.ACCESS_TOKEN, |
| | | OAuth2Constants.TOKEN_TYPE, |
| | | OAuth2Constants.EXPIRES_IN, |
| | | OAuth2Constants.STATE, |
| | | OAuth2Constants.ISSUER, |
| | | OAuth2Constants.ERROR, |
| | | OAuth2Constants.ERROR_DESCRIPTION, |
| | | OAuth2Constants.SESSION_STATE, |
| | | OAuth2Constants.RESPONSE, |
| | | Constants.KC_ACTION, |
| | | Constants.KC_ACTION_STATUS |
| | | ); |
| | | |
| | | 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 |