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;
- }
}