diff --git a/integration-tests/jakarta-ee/src/main/webapp/WEB-INF/web.xml b/integration-tests/jakarta-ee/src/main/webapp/WEB-INF/web.xml index 3008d063e4..cead2eaece 100644 --- a/integration-tests/jakarta-ee/src/main/webapp/WEB-INF/web.xml +++ b/integration-tests/jakarta-ee/src/main/webapp/WEB-INF/web.xml @@ -70,10 +70,6 @@ org.apache.shiro.form-resubmit.secure-cookies false - - org.apache.shiro.form-resubmit.whitelist.disabled - true - diff --git a/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/AuthenticationFilterDelegate.java b/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/AuthenticationFilterDelegate.java index fcc63912a5..7cb31df49f 100644 --- a/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/AuthenticationFilterDelegate.java +++ b/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/AuthenticationFilterDelegate.java @@ -33,15 +33,12 @@ import static org.apache.shiro.ee.filters.FormAuthenticationFilter.LOGIN_URL_ATTR_NAME; import static org.apache.shiro.ee.filters.FormAuthenticationFilter.LOGIN_WAITTIME_ATTR_NAME; import static org.apache.shiro.ee.filters.FormAuthenticationFilter.NO_PREDICATE; -import static org.apache.shiro.ee.filters.FormAuthenticationFilter.getPathWithinApplication; -import static org.apache.shiro.ee.filters.FormResubmitSupport.isPostRequest; import static org.apache.shiro.ee.filters.FormResubmitSupport.savePostDataForResubmit; import static org.apache.shiro.ee.filters.FormResubmitSupport.saveRequestReferer; import static org.apache.shiro.ee.filters.LogoutFilter.LOGOUT_PREDICATE_ATTR_NAME; import static org.apache.shiro.ee.filters.LogoutFilter.YES_PREDICATE; import static org.apache.shiro.ee.listeners.EnvironmentLoaderListener.isFormResubmitDisabled; import static org.apache.shiro.ee.listeners.EnvironmentLoaderListener.isServletNoPrincipal; -import static org.apache.shiro.web.filter.authc.NoAccessFilter.FORM_RESUBMIT_CHECK_SERVLET_PATH; import static org.apache.shiro.web.jaxrs.SubjectPrincipalRequestFilter.SHIRO_WEB_JAXRS_DISABLE_PRINCIPAL_PARAM; /** @@ -104,10 +101,7 @@ public boolean preHandle(ServletRequest request, ServletResponse response) throw public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { Subject subject = methods.getSubject(request, response); boolean isAuthenticated = subject.isAuthenticated() && subject.getPrincipal() != null; - return isAuthenticated || (useRemembered && subject.isRemembered()) - || (isPostRequest(request) - && FORM_RESUBMIT_CHECK_SERVLET_PATH.equals(getPathWithinApplication(request, - () -> WebUtils.getPathWithinApplication(WebUtils.toHttp(request))))); + return isAuthenticated || (useRemembered && subject.isRemembered()); } /** diff --git a/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/FormResubmitSupport.java b/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/FormResubmitSupport.java index c3e78686b6..775e8f5106 100644 --- a/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/FormResubmitSupport.java +++ b/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/FormResubmitSupport.java @@ -37,7 +37,6 @@ import java.net.URISyntaxException; import java.time.Duration; import java.util.Collections; -import org.apache.shiro.cache.CacheManager; import org.apache.shiro.crypto.CryptoException; import org.apache.shiro.ee.filters.Forms.FallbackPredicate; import static org.apache.shiro.ee.filters.FormResubmitSupportCookies.initializeCookies; @@ -59,9 +58,7 @@ import java.util.Set; import java.util.UUID; import static java.util.function.Predicate.not; -import static org.apache.shiro.ee.listeners.EnvironmentLoaderListener.isFormResubmitWhitelistEnabled; import static org.apache.shiro.ee.listeners.IniEnvironment.hasFacesContext; -import static org.apache.shiro.web.filter.authc.NoAccessFilter.FORM_RESUBMIT_CHECK_SERVLET_PATH; import static org.apache.shiro.web.filter.authz.PortFilter.DEFAULT_HTTP_PORT; import static org.apache.shiro.web.filter.authz.PortFilter.HTTP_SCHEME; import static org.apache.shiro.web.filter.authz.SslFilter.DEFAULT_HTTPS_PORT; @@ -106,10 +103,8 @@ public class FormResubmitSupport { static final String SHIRO_FORM_DATA_KEY = "org.apache.shiro.form-data-key"; static final String SESSION_EXPIRED_PARAMETER = "org.apache.shiro.sessionExpired"; static final String FORM_IS_RESUBMITTED = "org.apache.shiro.form-is-resubmitted"; - static final String FORM_RESUBMIT_WHITELIST = "org.apache.shiro.form-resubmit-whitelist"; static final String FORM_RESUBMIT_BLACKLIST = "org.apache.shiro.form-resubmit-blacklist"; static final String FORM_DATA_CACHE = "org.apache.shiro.form-data-cache"; - static final String FORM_DATA_KEY_PREFIX = "formDataKey="; // encoded view state private static final String FACES_VIEW_STATE = "jakarta.faces.ViewState"; private static final String FACES_VIEW_STATE_EQUALS = FACES_VIEW_STATE + "="; @@ -128,9 +123,6 @@ public class FormResubmitSupport { private static final Optional RESUBMIT_HOST = Optional.ofNullable(System.getProperty(FORM_RESUBMIT_HOST)); private static final Optional RESUBMIT_PORT = Optional.ofNullable(System.getProperty(FORM_RESUBMIT_PORT)) .map(Integer::valueOf); - private static final String FORM_RESUBMIT_WHITE_LIST_MAX_SIZE = "org.apache.shiro.form-resubmit-whitelist-max-size"; - private static final Optional RESUBMIT_WHITE_LIST_MAX_SIZE = - Optional.ofNullable(System.getProperty(FORM_RESUBMIT_WHITE_LIST_MAX_SIZE)).map(Integer::valueOf); private static final String FORM_RESUBMIT_BLACK_LIST_MAX_SIZE = "org.apache.shiro.form-resubmit-blacklist-max-size"; private static final Optional RESUBMIT_BLACK_LIST_MAX_SIZE = Optional.ofNullable(System.getProperty(FORM_RESUBMIT_BLACK_LIST_MAX_SIZE)).map(Integer::valueOf); @@ -234,7 +226,7 @@ static String getSavedFormDataFromKey(@NonNull UUID savedFormDataKey, Consumer - decrypt(encryptedData, rememberMeManager)).orElse(savedFormData); + decrypt(encryptedData, rememberMeManager)).orElse(null); } else { savedFormData = (String) cache.get(savedFormDataKey); } @@ -270,7 +262,7 @@ static String decrypt(String encrypted, AbstractRememberMeManager rememberMeMana static void saveRequest(HttpServletRequest request, HttpServletResponse response, boolean useReferer) { String path = useReferer ? getReferer(request) - : Servlets.getRequestURLWithQueryString(request); + : Servlets.getRequestURIWithQueryString(request); var rememberMeManager = getRememberMeManager(); if (path != null && rememberMeManager != null) { Servlets.addResponseCookie(request, response, WebUtils.SAVED_REQUEST_KEY, @@ -293,29 +285,34 @@ static void saveRequestReferer(boolean rv, HttpServletRequest request, HttpServl } static String getReferer(HttpServletRequest request) { - String referer = request.getHeader("referer"); - if (referer == null || referer.isBlank()) { + return normalizeSavedRequest(request.getHeader("referer"), request); + } + + static String normalizeSavedRequest(String savedRequest, HttpServletRequest request) { + if (savedRequest == null || savedRequest.isBlank()) { return null; } - try { - URI uri = URI.create(referer); - - String contextPath = WebUtils.getContextPath(request); - String path = WebUtils.normalize(uri.getPath()); - - if (path == null) { + URI uri = URI.create(savedRequest); + String rawPath = uri.getRawPath(); + if (rawPath == null || !rawPath.startsWith("/")) { + // opaque URI (mailto:, javascript:), or relative / empty path return null; } - + String path = uri.getPath(); + if (!path.equals(WebUtils.normalize(path))) { + // reject anything non-canonical: "//", "/./", "/../", and traversal + // above root (normalize returns null there, so equals() is false) + return null; + } + String contextPath = WebUtils.getContextPath(request); if (!contextPath.isEmpty() && !path.equals(contextPath) && !path.startsWith(contextPath + "/")) { return null; } - String query = uri.getRawQuery(); - return query == null ? path : path + "?" + query; + return query == null ? rawPath : rawPath + "?" + query; } catch (IllegalArgumentException e) { return null; } @@ -333,7 +330,8 @@ static String getReferer(HttpServletRequest request) { @SneakyThrows({IOException.class, InterruptedException.class}) static void redirectToSaved(HttpServletRequest request, HttpServletResponse response, FallbackPredicate useFallbackPath, String fallbackPath, boolean resubmit) { - String savedRequest = decrypt(Servlets.getRequestCookie(request, WebUtils.SAVED_REQUEST_KEY), getRememberMeManager()); + String savedRequest = normalizeSavedRequest(decrypt(Servlets.getRequestCookie(request, WebUtils.SAVED_REQUEST_KEY), + getRememberMeManager()), request); if (savedRequest != null) { doRedirectToSaved(request, response, savedRequest, resubmit); } else { @@ -368,8 +366,8 @@ private static void doRedirectToSaved(HttpServletRequest request, HttpServletRes String formData = getSavedFormDataFromKey(savedFormDataKey, cache::set); try { if (formData != null) { - Optional.ofNullable(resubmitSavedForm(formData, savedFormDataKeyString, savedRequest, - request, response, request.getServletContext(), false, true)) + Optional.ofNullable(resubmitSavedForm(formData, savedRequest, request, response, + request.getServletContext(), false, true)) .ifPresent(path -> doFacesRedirect(request, response, path)); doRedirectAtEnd = false; } else { @@ -445,7 +443,7 @@ static boolean isLoginUrl(HttpServletRequest request) { return loginUrl != null && request.getRequestURI().equals(request.getContextPath() + loginUrl); } - static String resubmitSavedForm(@NonNull String savedFormData, String savedFormDataKey, @NonNull String savedRequest, + static String resubmitSavedForm(@NonNull String savedFormData, @NonNull String rawSavedRequest, HttpServletRequest originalRequest, HttpServletResponse originalResponse, ServletContext servletContext, boolean rememberedAjaxResubmit, boolean redirect) throws InterruptedException, IOException { @@ -461,11 +459,17 @@ static String resubmitSavedForm(@NonNull String savedFormData, String savedFormD originalResponse.setStatus(AUTHFAIL); return resubmitResponseCleanup(originalRequest); } - URI overriddenRequestURI = overrideSavedRequestURI(URI.create(savedRequest)); + String savedRequest = normalizeSavedRequest(rawSavedRequest, originalRequest); + if (savedRequest == null) { + log.debug("Form resubmit: rejecting saved request"); + return originalRequest.getContextPath(); + } + URI overriddenRequestURI = overrideSavedRequestURI( + URI.create(Servlets.getRequestBaseURL(originalRequest)).resolve(savedRequest)); var cookieManager = new CookieManager(); HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)) .cookieHandler(cookieManager).build(); - if (!checkWhitelist(servletContext, overriddenRequestURI, client, savedFormDataKey)) { + if (isBlacklisted(overriddenRequestURI.getAuthority(), servletContext)) { return savedRequest; } initializeCookies(overriddenRequestURI, servletContext, cookieManager, originalRequest); @@ -476,9 +480,11 @@ static String resubmitSavedForm(@NonNull String savedFormData, String savedFormD HttpRequest postRequest = constructPostRequest(overriddenRequestURI, decodedFormData.result); response = sendResubmitRequest(client, postRequest); } catch (IOException e) { - log.warn("Unable to resubmit form to {}" + System.lineSeparator() + putBlacklistEntry(overriddenRequestURI.getAuthority(), servletContext); + log.warn("Unable to resubmit form to {}{}" + "perhaps set org.apache.shiro.form-resubmit-host or " - + "org.apache.shiro.form-resubmit-port system property?", overriddenRequestURI, e); + + "org.apache.shiro.form-resubmit-port system property?", + overriddenRequestURI, System.lineSeparator(), e); return savedRequest; } if (rememberedAjaxResubmit && !decodedFormData.isStatelessRequest) { @@ -487,7 +493,7 @@ static String resubmitSavedForm(@NonNull String savedFormData, String savedFormD log.debug("Redirect request: {}, response: {}", redirectRequest, redirectResponse); return processResubmitResponse(redirectResponse, originalRequest, originalResponse, response.headers(), savedRequest, servletContext, - true, rememberedAjaxResubmit, redirect); + true, true, redirect); } else { deleteCookie(originalResponse, servletContext, SHIRO_FORM_DATA_KEY); return processResubmitResponse(response, originalRequest, originalResponse, @@ -629,83 +635,53 @@ private static void setNoStoreHeaders(HttpServletResponse response) { response.setDateHeader(EXPIRES, 0); } - private static boolean checkWhitelist(ServletContext servletContext, URI savedRequestURI, HttpClient client, - String savedFormDataKey) { - if (!isSecurityManagerTypeOf(getSecurityManager(), DefaultSecurityManager.class)) { - log.warn("Shiro SecurityManager is not configured for form resubmit whitelist caching"); - return false; - } - DefaultSecurityManager dsm = getSecurityManager(DefaultSecurityManager.class); - if (dsm.getCacheManager() == null) { - log.warn("Shiro Cache manager is not configured, cannot cache form resubmit whitelist state"); - return false; - } - - Cache whitelist = getWhitelistCache(dsm); - Cache blacklist = getBlacklistCache(dsm); - String authority = savedRequestURI.getAuthority(); - - if (Boolean.TRUE.equals(whitelist.get(authority))) { - return true; - } else if (isBlacklisted(blacklist, authority)) { - log.debug("Form resubmit blacklist cache hit for {}", savedRequestURI); - return false; - } else if (checkWhitelistClient(savedRequestURI, servletContext.getContextPath(), client, - savedFormDataKey, dsm.getCacheManager())) { - if (isFormResubmitWhitelistEnabled(servletContext)) { - putWhitelistEntry(whitelist, authority); - } - blacklist.remove(authority); - return true; - } - - if (isFormResubmitBlacklistEnabled(servletContext)) { - putBlacklistEntry(blacklist, authority); - } - return false; - } - - static Cache getWhitelistCache(DefaultSecurityManager securityManager) { - return securityManager.getCacheManager().getCache(FORM_RESUBMIT_WHITELIST); - } - static Cache getBlacklistCache(DefaultSecurityManager securityManager) { + if (securityManager == null || securityManager.getCacheManager() == null) { + return null; + } return securityManager.getCacheManager().getCache(FORM_RESUBMIT_BLACKLIST); } - private static void putWhitelistEntry(Cache whitelist, String authority) { - if (whitelist.get(authority) == null) { - @SuppressWarnings("checkstyle:MagicNumber") - int maxSize = RESUBMIT_WHITE_LIST_MAX_SIZE.orElse(1000); - if (whitelist.size() >= maxSize) { - log.warn("Form resubmit whitelist exceeded max size of {}. Clearing whitelist.", maxSize); - whitelist.clear(); + private static void putBlacklistEntry(String authority, ServletContext servletContext) { + var blacklist = getBlacklistCache(getDefaultSecurityManager()); + if (blacklist != null && (servletContext == null || isFormResubmitBlacklistEnabled(servletContext))) { + if (blacklist.get(authority) == null) { + @SuppressWarnings("checkstyle:MagicNumber") + int maxSize = RESUBMIT_BLACK_LIST_MAX_SIZE.orElse(1000); + if (blacklist.size() >= maxSize) { + log.warn("Form resubmit blacklist exceeded max size of {}. Clearing blacklist.", maxSize); + blacklist.clear(); + } } + blacklist.put(authority, System.currentTimeMillis()); } - whitelist.put(authority, Boolean.TRUE); } - private static void putBlacklistEntry(Cache blacklist, String authority) { - if (blacklist.get(authority) == null) { - @SuppressWarnings("checkstyle:MagicNumber") - int maxSize = RESUBMIT_BLACK_LIST_MAX_SIZE.orElse(1000); - if (blacklist.size() >= maxSize) { - log.warn("Form resubmit blacklist exceeded max size of {}. Clearing blacklist.", maxSize); - blacklist.clear(); - } + private static DefaultSecurityManager getDefaultSecurityManager() { + if (!isSecurityManagerTypeOf(getSecurityManager(), DefaultSecurityManager.class)) { + log.debug("Shiro SecurityManager is not configured for form resubmit blacklist caching"); + return null; } - blacklist.put(authority, System.currentTimeMillis()); + DefaultSecurityManager dsm = getSecurityManager(DefaultSecurityManager.class); + if (dsm.getCacheManager() == null) { + log.debug("Shiro Cache manager is not configured, cannot cache form resubmit blacklist state"); + return null; + } + return dsm; } - static boolean isBlacklisted(Cache blacklist, String authority) { + static boolean isBlacklisted(String authority, ServletContext servletContext) { long currentTimeMillis = System.currentTimeMillis(); - return isBlacklisted(blacklist, authority, + return isBlacklisted(getBlacklistCache(getDefaultSecurityManager()), servletContext, authority, Duration.ofSeconds(RESUBMIT_BLACK_LIST_TTL_SECONDS.orElse(DEFAULT_RESUBMIT_BLACK_LIST_TTL_SECONDS)), currentTimeMillis); } - static boolean isBlacklisted(Cache blacklist, String authority, + static boolean isBlacklisted(Cache blacklist, ServletContext servletContext, String authority, Duration ttl, long currentTimeMillis) { + if (blacklist == null || (servletContext != null && !isFormResubmitBlacklistEnabled(servletContext))) { + return false; + } Long blacklistedAt = blacklist.get(authority); if (blacklistedAt == null) { return false; @@ -718,53 +694,6 @@ static boolean isBlacklisted(Cache blacklist, String authority, return active; } - private static boolean checkWhitelistClient(URI savedRequestURI, String contextPath, HttpClient client, - String savedFormDataKey, @NonNull CacheManager cacheManager) { - Cache cache = null; - UUID savedFormDataUUID = null; - - try { - var rememberMeManager = getRememberMeManager(); - if (rememberMeManager == null || rememberMeManager.getCipherService() == null - || rememberMeManager.getSerializer() == null) { - log.warn("Form resubmit cipher service not available, unable to decrypt - resubmit will not be available."); - return false; - } - - if (savedFormDataKey == null) { - savedFormDataUUID = UUID.randomUUID(); - savedFormDataKey = savedFormDataUUID.toString(); - cache = cacheManager.getCache(FORM_DATA_CACHE); - cache.put(savedFormDataUUID, "__DUMMY_FOR_CLIENT_WHITELIST_CHECK__"); - } - - var request = HttpRequest.newBuilder() - .uri(URI.create("%s://%s%s%s".formatted(savedRequestURI.getScheme(), savedRequestURI.getAuthority(), - contextPath, FORM_RESUBMIT_CHECK_SERVLET_PATH))) - .timeout(Duration.ofSeconds(3)).header(CONTENT_TYPE, "text/plain") - .POST(HttpRequest.BodyPublishers.ofString(rememberMeManager.getCipherService() - .encrypt((FORM_DATA_KEY_PREFIX + savedFormDataKey).getBytes(StandardCharsets.UTF_8), - rememberMeManager.getEncryptionCipherKey()).toBase64())).build(); - var response = client.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() == OK && Objects.equals(decrypt(response.body(), rememberMeManager), savedFormDataKey)) { - log.debug("Form resubmit whitelist check succeeded for {}", savedRequestURI); - return true; - } else { - log.debug("Form resubmit whitelist check failed for {} with status code {}", - savedRequestURI, response.statusCode()); - } - } catch (IOException | InterruptedException e) { - log.debug("Form resubmit whitelist check failed for {} with exception: {}", - savedRequestURI, e); - } finally { - if (cache != null) { - cache.remove(savedFormDataUUID); - } - } - return false; - } - public static DefaultWebSessionManager getNativeSessionManager(SecurityManager securityManager) { DefaultWebSessionManager rv = null; SecurityManager unwrapped = unwrapSecurityManager(securityManager, SecurityManager.class, type -> false); diff --git a/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/FormResubmitValidator.java b/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/FormResubmitValidator.java deleted file mode 100644 index 07cade479b..0000000000 --- a/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/FormResubmitValidator.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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.apache.shiro.ee.filters; - -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.apache.shiro.mgt.DefaultSecurityManager; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Optional; -import java.util.UUID; -import java.util.stream.Collectors; -import static org.apache.shiro.SecurityUtils.getSecurityManager; -import static org.apache.shiro.ee.filters.FormResubmitSupport.FORM_DATA_CACHE; -import static org.apache.shiro.ee.filters.FormResubmitSupport.FORM_DATA_KEY_PREFIX; -import static org.apache.shiro.ee.filters.FormResubmitSupport.decrypt; -import static org.apache.shiro.ee.filters.FormResubmitSupport.getRememberMeManager; -import static org.apache.shiro.web.filter.authc.NoAccessFilter.FORM_RESUBMIT_CHECK_SERVLET_PATH; - -@Slf4j -@WebServlet(name = "ShiroFormResubmitValidator", urlPatterns = FORM_RESUBMIT_CHECK_SERVLET_PATH) -public class FormResubmitValidator extends HttpServlet { - @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) { - var rememberMeManager = getRememberMeManager(); - if (rememberMeManager == null || rememberMeManager.getCipherService() == null) { - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - } else { - try { - String formDataKey = decrypt(request.getReader().lines().collect(Collectors.joining()), rememberMeManager) - .substring(FORM_DATA_KEY_PREFIX.length()); - var cache = getSecurityManager(DefaultSecurityManager.class) - .getCacheManager().getCache(FORM_DATA_CACHE); - Optional.ofNullable(cache.get(UUID.fromString(formDataKey))).orElseThrow(IllegalCallerException::new); - String encryptedFormDataKey = rememberMeManager.getCipherService() - .encrypt(formDataKey.getBytes(StandardCharsets.UTF_8), - rememberMeManager.getEncryptionCipherKey()).toBase64(); - response.getWriter().write(encryptedFormDataKey); - response.setStatus(HttpServletResponse.SC_OK); - } catch (IOException | IllegalCallerException e) { - log.warn("Form resubmit verification: invalid input or failed to write encrypted session id to response", e); - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - } - } - } -} diff --git a/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/ShiroFilter.java b/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/ShiroFilter.java index 9413d1261c..0576b3d99d 100644 --- a/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/ShiroFilter.java +++ b/support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/ShiroFilter.java @@ -245,8 +245,8 @@ protected void executeChain(ServletRequest request, ServletResponse response, log.debug("Resubmitting Post Data: {}", postData); var httpRequest = WebUtils.toHttp(request); boolean rememberedAjaxResubmit = "partial/ajax".equals(httpRequest.getHeader("Faces-Request")); - Optional.ofNullable(resubmitSavedForm(postData, null, - Servlets.getRequestURLWithQueryString(httpRequest), + Optional.ofNullable(resubmitSavedForm(postData, + Servlets.getRequestURIWithQueryString(httpRequest), WebUtils.toHttp(request), WebUtils.toHttp(response), request.getServletContext(), rememberedAjaxResubmit, false)) .ifPresent(url -> sendRedirect(response, url)); diff --git a/support/jakarta-ee/src/main/java/org/apache/shiro/ee/listeners/EnvironmentLoaderListener.java b/support/jakarta-ee/src/main/java/org/apache/shiro/ee/listeners/EnvironmentLoaderListener.java index 5d49862ec5..cc47fade83 100644 --- a/support/jakarta-ee/src/main/java/org/apache/shiro/ee/listeners/EnvironmentLoaderListener.java +++ b/support/jakarta-ee/src/main/java/org/apache/shiro/ee/listeners/EnvironmentLoaderListener.java @@ -46,7 +46,6 @@ public class EnvironmentLoaderListener extends EnvironmentLoader implements Serv private static final String SHIRO_EE_CHAR_ENCODING_PARAM = "org.apache.shiro.ee.character-encoding"; private static final String FORM_RESUBMIT_DISABLED_PARAM = "org.apache.shiro.form-resubmit.disabled"; private static final String FORM_RESUBMIT_SECURE_COOKIES = "org.apache.shiro.form-resubmit.secure-cookies"; - private static final String FORM_RESUBMIT_WHITE_LIST_DISABLED = "org.apache.shiro.form-resubmit.whitelist.disabled"; private static final String FORM_RESUBMIT_BLACK_LIST_DISABLED = "org.apache.shiro.form-resubmit.blacklist.disabled"; private static final String SHIRO_WEB_DISABLE_PRINCIPAL_PARAM = "org.apache.shiro.web.disable-principal"; @@ -66,10 +65,6 @@ public static boolean isFormResubmitSecureCookies(ServletContext ctx) { return Boolean.TRUE.equals(ctx.getAttribute(FORM_RESUBMIT_SECURE_COOKIES)); } - public static boolean isFormResubmitWhitelistEnabled(ServletContext ctx) { - return !Boolean.TRUE.equals(ctx.getAttribute(FORM_RESUBMIT_WHITE_LIST_DISABLED)); - } - public static boolean isFormResubmitBlacklistEnabled(ServletContext ctx) { return !Boolean.TRUE.equals(ctx.getAttribute(FORM_RESUBMIT_BLACK_LIST_DISABLED)); } @@ -106,9 +101,6 @@ public void contextInitialized(ServletContextEvent sce) { } else { sce.getServletContext().setAttribute(FORM_RESUBMIT_SECURE_COOKIES, Boolean.FALSE); } - if (Boolean.parseBoolean(sce.getServletContext().getInitParameter(FORM_RESUBMIT_WHITE_LIST_DISABLED))) { - sce.getServletContext().setAttribute(FORM_RESUBMIT_WHITE_LIST_DISABLED, Boolean.TRUE); - } if (Boolean.parseBoolean(sce.getServletContext().getInitParameter(FORM_RESUBMIT_BLACK_LIST_DISABLED))) { sce.getServletContext().setAttribute(FORM_RESUBMIT_BLACK_LIST_DISABLED, Boolean.TRUE); } diff --git a/support/jakarta-ee/src/test/java/org/apache/shiro/ee/filters/FormSupportTest.java b/support/jakarta-ee/src/test/java/org/apache/shiro/ee/filters/FormSupportTest.java index 36ab39bf52..afb1d2d2de 100644 --- a/support/jakarta-ee/src/test/java/org/apache/shiro/ee/filters/FormSupportTest.java +++ b/support/jakarta-ee/src/test/java/org/apache/shiro/ee/filters/FormSupportTest.java @@ -13,6 +13,7 @@ */ package org.apache.shiro.ee.filters; +import jakarta.servlet.ServletContext; import org.apache.shiro.ee.filters.FormResubmitSupport.PartialAjaxResult; import org.apache.shiro.cache.MemoryConstrainedCacheManager; @@ -48,12 +49,15 @@ * Resubmit forms support */ @ExtendWith(MockitoExtension.class) +@SuppressWarnings("checkstyle:MethodCount") class FormSupportTest { private static final long BLACKLISTED_AT = 1_000L; private static final Duration BLACKLIST_TTL = Duration.ofSeconds(60); @Mock private HttpServletRequest request; + @Mock + private ServletContext servletContext; @Test void nullReferer() { @@ -70,7 +74,6 @@ void blankReferer() { @Test void plainStringReferer() { when(request.getHeader("referer")).thenReturn("hello"); - when(request.getContextPath()).thenReturn("/myapp"); assertThat(getReferer(request)).isNull(); } @@ -129,34 +132,26 @@ void rootContextKeepsPathWithQuery() { } @Test - void normalizedPathWithinContextIsAccepted() { + void nonCanonicalPathIsRejected() { when(request.getHeader("referer")).thenReturn("https://example.com/myapp//foo/./bar.xhtml"); - when(request.getContextPath()).thenReturn("/myapp"); - - assertThat(getReferer(request)).isEqualTo("/myapp/foo/bar.xhtml"); + assertThat(getReferer(request)).isNull(); } @Test void normalizedPathEscapingContextIsRejected() { when(request.getHeader("referer")).thenReturn("https://example.com/myapp/../otherapp/page.xhtml"); - when(request.getContextPath()).thenReturn("/myapp"); - assertThat(getReferer(request)).isNull(); } @Test void opaqueUriRefererIsRejected() { when(request.getHeader("referer")).thenReturn("mailto:test@example.com"); - when(request.getContextPath()).thenReturn("/myapp"); - assertThat(getReferer(request)).isNull(); } @Test void javascriptUriRefererIsRejected() { when(request.getHeader("referer")).thenReturn("javascript:alert(1)"); - when(request.getContextPath()).thenReturn("/myapp"); - assertThat(getReferer(request)).isNull(); } @@ -177,7 +172,7 @@ void refererWithFragmentDropsFragmentAndKeepsQueryOnly() { } @Test - void externalHostWithMatchingContextCurrentlyPasses() { + void externalHostIsStrippedToPath() { when(request.getHeader("referer")).thenReturn("https://attacker.example/myapp/login.xhtml"); when(request.getContextPath()).thenReturn("/myapp"); @@ -186,19 +181,25 @@ void externalHostWithMatchingContextCurrentlyPasses() { @Test void encodedPathTraversalRefererIsRejected() { - when(request.getHeader("referer")) - .thenReturn("https://example.com/myapp/%2e%2e/otherapp/page.xhtml"); - when(request.getContextPath()).thenReturn("/myapp"); - + when(request.getHeader("referer")).thenReturn("https://example.com/myapp/%2e%2e/otherapp/page.xhtml"); assertThat(getReferer(request)).isNull(); } @Test void encodedPathTraversalWithEncodedSlashesRefererIsRejected() { - when(request.getHeader("referer")) - .thenReturn("https://example.com/myapp/%2e%2e%2fotherapp%2fpage.xhtml"); - when(request.getContextPath()).thenReturn("/myapp"); + when(request.getHeader("referer")).thenReturn("https://example.com/myapp/%2e%2e%2fotherapp%2fpage.xhtml"); + assertThat(getReferer(request)).isNull(); + } + @Test + void doubleSlashPathWithRootContextIsRejected() { + when(request.getHeader("referer")).thenReturn("https://example.com//evil.com/x"); + assertThat(getReferer(request)).isNull(); + } + + @Test + void doubleSlashPathWithinContextIsRejected() { + when(request.getHeader("referer")).thenReturn("https://attacker.example//myapp/x"); assertThat(getReferer(request)).isNull(); } @@ -348,18 +349,15 @@ void parseCookies() { @Test @SuppressWarnings("checkstyle:MagicNumber") - void whitelistAndBlacklistUseShiroCacheManager() { + void blacklistUseShiroCacheManager() { var securityManager = new DefaultSecurityManager(); securityManager.setCacheManager(new MemoryConstrainedCacheManager()); - var whitelist = FormResubmitSupport.getWhitelistCache(securityManager); var blacklist = FormResubmitSupport.getBlacklistCache(securityManager); - whitelist.put("good.example", Boolean.TRUE); blacklist.put("bad.example", BLACKLISTED_AT); - assertThat(FormResubmitSupport.getWhitelistCache(securityManager).get("good.example")).isTrue(); - assertThat(FormResubmitSupport.isBlacklisted(blacklist, "bad.example", + assertThat(FormResubmitSupport.isBlacklisted(blacklist, null, "bad.example", BLACKLIST_TTL, 1_500L)).isTrue(); } @@ -372,11 +370,28 @@ void expiredBlacklistEntryIsRemovedFromShiroCache() { var blacklist = FormResubmitSupport.getBlacklistCache(securityManager); blacklist.put("expired.example", BLACKLISTED_AT); - assertThat(FormResubmitSupport.isBlacklisted(blacklist, "expired.example", + assertThat(FormResubmitSupport.isBlacklisted(blacklist, null, "expired.example", BLACKLIST_TTL, 61_001L)).isFalse(); assertThat(blacklist.get("expired.example")).isNull(); } + @Test + @SuppressWarnings("checkstyle:MagicNumber") + void blacklistHonoursEnabledFlag() { + var securityManager = new DefaultSecurityManager(); + securityManager.setCacheManager(new MemoryConstrainedCacheManager()); + var blacklist = FormResubmitSupport.getBlacklistCache(securityManager); + blacklist.put("bad.example", BLACKLISTED_AT); + + // attribute absent → enabled + assertThat(FormResubmitSupport.isBlacklisted(blacklist, servletContext, "bad.example", + BLACKLIST_TTL, 1_500L)).isTrue(); + + when(servletContext.getAttribute("org.apache.shiro.form-resubmit.blacklist.disabled")).thenReturn(Boolean.TRUE); + assertThat(FormResubmitSupport.isBlacklisted(blacklist, servletContext, "bad.example", + BLACKLIST_TTL, 1_500L)).isFalse(); + } + private static String decode(String plain) { return URLDecoder.decode(plain, StandardCharsets.UTF_8); } diff --git a/web/src/main/java/org/apache/shiro/web/filter/authc/NoAccessFilter.java b/web/src/main/java/org/apache/shiro/web/filter/authc/NoAccessFilter.java index 4b26142341..e3380164f8 100644 --- a/web/src/main/java/org/apache/shiro/web/filter/authc/NoAccessFilter.java +++ b/web/src/main/java/org/apache/shiro/web/filter/authc/NoAccessFilter.java @@ -18,7 +18,6 @@ */ package org.apache.shiro.web.filter.authc; -import jakarta.servlet.http.HttpServletRequest; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.web.util.WebUtils; import org.slf4j.Logger; @@ -32,8 +31,6 @@ * that do not match existing filter patterns. */ public class NoAccessFilter extends AuthenticatingFilter { - public static final String FORM_RESUBMIT_CHECK_SERVLET_PATH = "/org.apache.shiro.form-resubmit-check"; - private final Logger log = LoggerFactory.getLogger(NoAccessFilter.class); @Override @@ -47,14 +44,4 @@ protected boolean onAccessDenied(ServletRequest request, ServletResponse respons protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { return null; } - - @Override - protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { - if (request instanceof HttpServletRequest) { - HttpServletRequest httpRequest = WebUtils.toHttp(request); - return httpRequest.getMethod().equals("POST") - && httpRequest.getServletPath().equals(FORM_RESUBMIT_CHECK_SERVLET_PATH); - } - return false; - } }