Skip to content

Commit 1f95551

Browse files
committed
bugfix: update form resubmit handling to use encrypted form data key and improve cache management
1 parent 3e4f9e1 commit 1f95551

4 files changed

Lines changed: 58 additions & 39 deletions

File tree

support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/FormResubmitSupport.java

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import static jakarta.faces.application.StateManager.STATE_SAVING_METHOD_CLIENT;
1717
import static jakarta.faces.application.StateManager.STATE_SAVING_METHOD_PARAM_NAME;
18+
import static jakarta.ws.rs.core.MediaType.TEXT_PLAIN;
1819
import static org.apache.shiro.SecurityUtils.getSecurityManager;
1920
import static org.apache.shiro.SecurityUtils.isSecurityManagerTypeOf;
2021
import static org.apache.shiro.SecurityUtils.unwrapSecurityManager;
@@ -58,6 +59,8 @@
5859
import static org.apache.shiro.ee.listeners.IniEnvironment.hasFacesContext;
5960
import static org.apache.shiro.web.filter.authc.NoAccessFilter.FORM_RESUBMIT_CHECK_SERVLET_PATH;
6061
import static org.apache.shiro.web.mgt.CookieRememberMeManager.DEFAULT_REMEMBER_ME_COOKIE_NAME;
62+
import java.util.concurrent.atomic.AtomicReference;
63+
import java.util.function.Consumer;
6164
import java.util.regex.Pattern;
6265
import java.util.stream.Collectors;
6366
import jakarta.servlet.ServletContext;
@@ -99,6 +102,7 @@ public class FormResubmitSupport {
99102
static final String FORM_IS_RESUBMITTED = "org.apache.shiro.form-is-resubmitted";
100103
static final String FORM_RESUBMIT_WHITELIST = "org.apache.shiro.form-resubmit-whitelist";
101104
static final String FORM_RESUBMIT_BLACKLIST = "org.apache.shiro.form-resubmit-blacklist";
105+
static final String FORM_DATA_CACHE = "org.apache.shiro.form-data-cache";
102106
// encoded view state
103107
private static final String FACES_VIEW_STATE = "jakarta.faces.ViewState";
104108
private static final String FACES_VIEW_STATE_EQUALS = FACES_VIEW_STATE + "=";
@@ -112,7 +116,6 @@ public class FormResubmitSupport {
112116
= Pattern.compile("[\\&]?(%s.\\w+|%s.\\w+|%s)=[\\w\\s:%%\\d]*".formatted(
113117
"jakarta.faces.partial", "jakarta.faces.behavior", FACES_SOURCE));
114118
private static final Pattern INITIAL_AMPERSAND = Pattern.compile("^\\&");
115-
private static final String FORM_DATA_CACHE = "org.apache.shiro.form-data-cache";
116119
private static final String FORM_RESUBMIT_HOST = "org.apache.shiro.form-resubmit-host";
117120
private static final String FORM_RESUBMIT_PORT = "org.apache.shiro.form-resubmit-port";
118121
private static final Optional<String> RESUBMIT_HOST = Optional.ofNullable(System.getProperty(FORM_RESUBMIT_HOST));
@@ -203,7 +206,7 @@ static String getPostData(ServletRequest request) {
203206
return request.getReader().lines().collect(Collectors.joining());
204207
}
205208

206-
static String getSavedFormDataFromKey(@NonNull String savedFormDataKey) {
209+
static String getSavedFormDataFromKey(@NonNull String savedFormDataKey, Consumer<Cache<Object, ?>> cacheConsumer) {
207210
String savedFormData = null;
208211
if (isSecurityManagerTypeOf(getSecurityManager(), DefaultSecurityManager.class)) {
209212
DefaultSecurityManager dsm = getSecurityManager(DefaultSecurityManager.class);
@@ -218,7 +221,7 @@ static String getSavedFormDataFromKey(@NonNull String savedFormDataKey) {
218221
} else {
219222
savedFormData = (String) cache.get(cacheKey);
220223
}
221-
cache.remove(cacheKey);
224+
cacheConsumer.accept(cache);
222225
}
223226
}
224227
return savedFormData;
@@ -343,14 +346,21 @@ private static void doRedirectToSaved(HttpServletRequest request, HttpServletRes
343346
String savedFormDataKey = Servlets.getRequestCookie(request, SHIRO_FORM_DATA_KEY);
344347
boolean doRedirectAtEnd = true;
345348
if (savedFormDataKey != null && resubmit) {
346-
String formData = getSavedFormDataFromKey(savedFormDataKey);
347-
if (formData != null) {
348-
Optional.ofNullable(resubmitSavedForm(formData, savedRequest,
349-
request, response, request.getServletContext(), false, true))
350-
.ifPresent(path -> doFacesRedirect(request, response, path));
351-
doRedirectAtEnd = false;
352-
} else {
353-
deleteCookie(response, request.getServletContext(), SHIRO_FORM_DATA_KEY);
349+
AtomicReference<Cache<Object, ?>> cache = new AtomicReference<>();
350+
String formData = getSavedFormDataFromKey(savedFormDataKey, cache::set);
351+
try {
352+
if (formData != null) {
353+
Optional.ofNullable(resubmitSavedForm(formData, savedFormDataKey, savedRequest,
354+
request, response, request.getServletContext(), false, true))
355+
.ifPresent(path -> doFacesRedirect(request, response, path));
356+
doRedirectAtEnd = false;
357+
} else {
358+
deleteCookie(response, request.getServletContext(), SHIRO_FORM_DATA_KEY);
359+
}
360+
} finally {
361+
if (cache.get() != null) {
362+
cache.get().remove(savedFormDataKey);
363+
}
354364
}
355365
}
356366
if (doRedirectAtEnd) {
@@ -417,7 +427,7 @@ static boolean isLoginUrl(HttpServletRequest request) {
417427
return loginUrl != null && request.getRequestURI().equals(request.getContextPath() + loginUrl);
418428
}
419429

420-
static String resubmitSavedForm(@NonNull String savedFormData, @NonNull String savedRequest,
430+
static String resubmitSavedForm(@NonNull String savedFormData, String savedFormDataKey, @NonNull String savedRequest,
421431
HttpServletRequest originalRequest, HttpServletResponse originalResponse,
422432
ServletContext servletContext, boolean rememberedAjaxResubmit, boolean redirect)
423433
throws InterruptedException, IOException {
@@ -433,10 +443,13 @@ static String resubmitSavedForm(@NonNull String savedFormData, @NonNull String s
433443
return resubmitResponseCleanup(originalRequest);
434444
}
435445
URI overriddenRequestURI = overrideSavedRequestURI(URI.create(savedRequest));
436-
HttpClient client = buildHttpClient(overriddenRequestURI, servletContext, originalRequest);
437-
if (!checkWhitelist(servletContext, overriddenRequestURI, client)) {
446+
var cookieManager = new CookieManager();
447+
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2))
448+
.cookieHandler(cookieManager).build();
449+
if (!checkWhitelist(servletContext, overriddenRequestURI, client, savedFormDataKey)) {
438450
return savedRequest;
439451
}
452+
initializeCookies(overriddenRequestURI, servletContext, cookieManager, originalRequest);
440453
HttpResponse<String> response;
441454
PartialAjaxResult decodedFormData;
442455
try {
@@ -564,9 +577,8 @@ private static String resubmitResponseCleanup(HttpServletRequest originalRequest
564577
return null;
565578
}
566579

567-
private static HttpClient buildHttpClient(URI savedRequest, ServletContext servletContext,
568-
HttpServletRequest originalRequest) {
569-
CookieManager cookieManager = new CookieManager();
580+
private static void initializeCookies(URI savedRequest, ServletContext servletContext,
581+
CookieManager cookieManager, HttpServletRequest originalRequest) {
570582
var session = SecurityUtils.getSubject().getSession();
571583
var sessionCookieName = getSessionCookieName(servletContext, getSecurityManager());
572584
var sessionCookie = new HttpCookie(sessionCookieName, session.getId().toString());
@@ -589,10 +601,10 @@ private static HttpClient buildHttpClient(URI savedRequest, ServletContext servl
589601
}
590602
}
591603
}
592-
return HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).cookieHandler(cookieManager).build();
593604
}
594605

595-
private static boolean checkWhitelist(ServletContext servletContext, URI savedRequestURI, HttpClient client) {
606+
private static boolean checkWhitelist(ServletContext servletContext, URI savedRequestURI, HttpClient client,
607+
String savedFormDataKey) {
596608
if (!isSecurityManagerTypeOf(getSecurityManager(), DefaultSecurityManager.class)) {
597609
log.warn("Shiro SecurityManager is not configured for form resubmit whitelist caching");
598610
return false;
@@ -612,7 +624,7 @@ private static boolean checkWhitelist(ServletContext servletContext, URI savedRe
612624
} else if (isBlacklisted(blacklist, authority)) {
613625
log.debug("Form resubmit blacklist cache hit for {}", savedRequestURI);
614626
return false;
615-
} else if (checkWhitelistClient(savedRequestURI, servletContext.getContextPath(), client)) {
627+
} else if (checkWhitelistClient(savedRequestURI, servletContext.getContextPath(), client, savedFormDataKey)) {
616628
putWhitelistEntry(whitelist, authority);
617629
blacklist.remove(authority);
618630
return true;
@@ -675,7 +687,8 @@ static boolean isBlacklisted(Cache<String, Long> blacklist, String authority,
675687
return active;
676688
}
677689

678-
private static boolean checkWhitelistClient(URI savedRequestURI, String contextPath, HttpClient client) {
690+
private static boolean checkWhitelistClient(URI savedRequestURI, String contextPath, HttpClient client,
691+
String savedFormDataKey) {
679692
try {
680693
var rememberMeManager = getRememberMeManager();
681694
if (rememberMeManager == null || rememberMeManager.getCipherService() == null
@@ -687,11 +700,13 @@ private static boolean checkWhitelistClient(URI savedRequestURI, String contextP
687700
var request = HttpRequest.newBuilder()
688701
.uri(URI.create("%s://%s%s%s".formatted(savedRequestURI.getScheme(), savedRequestURI.getAuthority(),
689702
contextPath, FORM_RESUBMIT_CHECK_SERVLET_PATH)))
690-
.timeout(Duration.ofSeconds(3)).GET().build();
703+
.timeout(Duration.ofSeconds(3)).header(CONTENT_TYPE, TEXT_PLAIN)
704+
.POST(HttpRequest.BodyPublishers.ofString(rememberMeManager.getCipherService()
705+
.encrypt(savedFormDataKey.getBytes(StandardCharsets.UTF_8),
706+
rememberMeManager.getEncryptionCipherKey()).toBase64())).build();
691707
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
692708

693-
if (response.statusCode() == OK && Objects.equals(decrypt(response.body(), rememberMeManager),
694-
SecurityUtils.getSubject().getSession().getId().toString())) {
709+
if (response.statusCode() == OK && Objects.equals(decrypt(response.body(), rememberMeManager), savedFormDataKey)) {
695710
log.debug("Form resubmit whitelist check succeeded for {}", savedRequestURI);
696711
return true;
697712
} else {

support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/FormResubmitValidator.java

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,35 +23,39 @@
2323
import jakarta.servlet.http.HttpServletRequest;
2424
import jakarta.servlet.http.HttpServletResponse;
2525
import lombok.extern.slf4j.Slf4j;
26-
import org.apache.shiro.SecurityUtils;
26+
import org.apache.shiro.mgt.DefaultSecurityManager;
2727
import java.io.IOException;
2828
import java.nio.charset.StandardCharsets;
29+
import java.util.Optional;
30+
import java.util.UUID;
31+
import java.util.stream.Collectors;
32+
import static org.apache.shiro.SecurityUtils.getSecurityManager;
33+
import static org.apache.shiro.ee.filters.FormResubmitSupport.FORM_DATA_CACHE;
34+
import static org.apache.shiro.ee.filters.FormResubmitSupport.decrypt;
2935
import static org.apache.shiro.ee.filters.FormResubmitSupport.getRememberMeManager;
3036
import static org.apache.shiro.web.filter.authc.NoAccessFilter.FORM_RESUBMIT_CHECK_SERVLET_PATH;
3137

3238
@Slf4j
3339
@WebServlet(name = "ShiroFormResubmitValidator", urlPatterns = FORM_RESUBMIT_CHECK_SERVLET_PATH)
3440
public class FormResubmitValidator extends HttpServlet {
3541
@Override
36-
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
37-
var session = SecurityUtils.getSubject().getSession(false);
38-
if (session == null) {
39-
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
40-
return;
41-
}
42-
42+
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
4343
var rememberMeManager = getRememberMeManager();
4444
if (rememberMeManager == null || rememberMeManager.getCipherService() == null) {
4545
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
4646
} else {
4747
try {
48-
String encryptedSessionId = rememberMeManager.getCipherService()
49-
.encrypt(session.getId().toString().getBytes(StandardCharsets.UTF_8),
48+
String formDataKey = decrypt(request.getReader().lines().collect(Collectors.joining()), rememberMeManager);
49+
var cache = getSecurityManager(DefaultSecurityManager.class)
50+
.getCacheManager().getCache(FORM_DATA_CACHE);
51+
Optional.ofNullable(cache.get(UUID.fromString(formDataKey))).orElseThrow(IllegalCallerException::new);
52+
String encryptedFormDataKey = rememberMeManager.getCipherService()
53+
.encrypt(formDataKey.getBytes(StandardCharsets.UTF_8),
5054
rememberMeManager.getEncryptionCipherKey()).toBase64();
51-
response.getWriter().write(encryptedSessionId);
55+
response.getWriter().write(encryptedFormDataKey);
5256
response.setStatus(HttpServletResponse.SC_OK);
53-
} catch (IOException e) {
54-
log.warn("Form resubmit verification: failed to write encrypted principals to response", e);
57+
} catch (IOException | IllegalCallerException e) {
58+
log.warn("Form resubmit verification: invalid input or failed to write encrypted session id to response", e);
5559
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
5660
}
5761
}

support/jakarta-ee/src/main/java/org/apache/shiro/ee/filters/ShiroFilter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ protected void executeChain(ServletRequest request, ServletResponse response,
245245
log.debug("Resubmitting Post Data: {}", postData);
246246
var httpRequest = WebUtils.toHttp(request);
247247
boolean rememberedAjaxResubmit = "partial/ajax".equals(httpRequest.getHeader("Faces-Request"));
248-
Optional.ofNullable(resubmitSavedForm(postData,
248+
Optional.ofNullable(resubmitSavedForm(postData, null,
249249
Servlets.getRequestURLWithQueryString(httpRequest),
250250
WebUtils.toHttp(request), WebUtils.toHttp(response),
251251
request.getServletContext(), rememberedAjaxResubmit, false))

web/src/main/java/org/apache/shiro/web/filter/authc/NoAccessFilter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ protected AuthenticationToken createToken(ServletRequest request, ServletRespons
5252
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
5353
if (request instanceof HttpServletRequest) {
5454
HttpServletRequest httpRequest = WebUtils.toHttp(request);
55-
return httpRequest.getMethod().equals("GET")
55+
return httpRequest.getMethod().equals("POST")
5656
&& httpRequest.getServletPath().equals(FORM_RESUBMIT_CHECK_SERVLET_PATH);
5757
}
5858
return false;

0 commit comments

Comments
 (0)