This is an automated email from the ASF dual-hosted git repository.
lprimak pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/shiro.git
The following commit(s) were added to refs/heads/main by this push:
new 8e0e4ac7a bugfix(jakarta-ee): form resubmit logic uses more data from
the subse… (#2885)
8e0e4ac7a is described below
commit 8e0e4ac7a7af35c8c8ec552b51cf3bd809196622
Author: Lenny Primak <[email protected]>
AuthorDate: Thu Sep 10 09:54:50 2026 -0500
bugfix(jakarta-ee): form resubmit logic uses more data from the subse…
(#2885)
* bugfix(jakarta-ee): form resubmit logic uses more data from the
subsequent request, rather than previous request
* refactor: deleted now-obsolete whitelist check
* fixed errors in AuthenticationFilterDelegate.java
---
.../jakarta-ee/src/main/webapp/WEB-INF/web.xml | 4 -
.../ee/filters/AuthenticationFilterDelegate.java | 8 +-
.../shiro/ee/filters/FormResubmitSupport.java | 205 +++++++--------------
.../shiro/ee/filters/FormResubmitValidator.java | 65 -------
.../org/apache/shiro/ee/filters/ShiroFilter.java | 4 +-
.../ee/listeners/EnvironmentLoaderListener.java | 8 -
.../apache/shiro/ee/filters/FormSupportTest.java | 65 ++++---
.../shiro/web/filter/authc/NoAccessFilter.java | 13 --
8 files changed, 110 insertions(+), 262 deletions(-)
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 3008d063e..cead2eaec 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 @@
<param-name>org.apache.shiro.form-resubmit.secure-cookies</param-name>
<param-value>false</param-value>
</context-param>
- <context-param>
-
<param-name>org.apache.shiro.form-resubmit.whitelist.disabled</param-name>
- <param-value>true</param-value>
- </context-param>
<!-- Apache Shiro Security -->
<context-param>
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 fcc63912a..7cb31df49 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_PREDICA
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 @@ class AuthenticationFilterDelegate {
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 c3e78686b..775e8f510 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 static
org.apache.shiro.ee.filters.FormResubmitSupportCookies.getSessionC
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.Optional;
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<String> RESUBMIT_HOST =
Optional.ofNullable(System.getProperty(FORM_RESUBMIT_HOST));
private static final Optional<Integer> 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<Integer> 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<Integer> RESUBMIT_BLACK_LIST_MAX_SIZE =
Optional.ofNullable(System.getProperty(FORM_RESUBMIT_BLACK_LIST_MAX_SIZE)).map(Integer::valueOf);
@@ -234,7 +226,7 @@ public class FormResubmitSupport {
if (rememberMeManager != null &&
rememberMeManager.getCipherService() != null) {
var cachedData = Optional.ofNullable((byte[])
cache.get(savedFormDataKey));
savedFormData = cachedData.map(encryptedData ->
- decrypt(encryptedData,
rememberMeManager)).orElse(savedFormData);
+ decrypt(encryptedData,
rememberMeManager)).orElse(null);
} else {
savedFormData = (String) cache.get(savedFormDataKey);
}
@@ -270,7 +262,7 @@ public class FormResubmitSupport {
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 @@ public class FormResubmitSupport {
}
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 @@ public class FormResubmitSupport {
@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 @@ public class FormResubmitSupport {
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 @@ public class FormResubmitSupport {
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 @@ public class FormResubmitSupport {
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 @@ public class FormResubmitSupport {
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 @@ public class FormResubmitSupport {
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 @@ public class FormResubmitSupport {
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<String, Boolean> whitelist = getWhitelistCache(dsm);
- Cache<String, Long> 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<String, Boolean> getWhitelistCache(DefaultSecurityManager
securityManager) {
- return
securityManager.getCacheManager().getCache(FORM_RESUBMIT_WHITELIST);
- }
-
static Cache<String, Long> getBlacklistCache(DefaultSecurityManager
securityManager) {
+ if (securityManager == null || securityManager.getCacheManager() ==
null) {
+ return null;
+ }
return
securityManager.getCacheManager().getCache(FORM_RESUBMIT_BLACKLIST);
}
- private static void putWhitelistEntry(Cache<String, Boolean> 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<String, Long> 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<String, Long> 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<String, Long> blacklist, String
authority,
+ static boolean isBlacklisted(Cache<String, Long> 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 @@ public class FormResubmitSupport {
return active;
}
- private static boolean checkWhitelistClient(URI savedRequestURI, String
contextPath, HttpClient client,
- String savedFormDataKey,
@NonNull CacheManager cacheManager) {
- Cache<UUID, String> 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 07cade479..000000000
---
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 9413d1261..0576b3d99 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 @@ public class ShiroFilter extends
org.apache.shiro.web.servlet.ShiroFilter {
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 5d49862ec..cc47fade8 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 class EnvironmentLoaderListener extends
EnvironmentLoader implements Serv
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 class EnvironmentLoaderListener extends
EnvironmentLoader implements Serv
} 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 36ab39bf5..afb1d2d2d 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 @@ import org.apache.shiro.mgt.DefaultSecurityManager;
* 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 @@ class FormSupportTest {
@Test
void plainStringReferer() {
when(request.getHeader("referer")).thenReturn("hello");
- when(request.getContextPath()).thenReturn("/myapp");
assertThat(getReferer(request)).isNull();
}
@@ -129,34 +132,26 @@ class FormSupportTest {
}
@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:[email protected]");
- 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 @@ class FormSupportTest {
}
@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 @@ class FormSupportTest {
@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 @@ class FormSupportTest {
@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 @@ class FormSupportTest {
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 4b2614234..e3380164f 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 @@ import jakarta.servlet.ServletResponse;
* 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 @@ public class NoAccessFilter extends AuthenticatingFilter {
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;
- }
}