Skip to content

Commit 25ee2c7

Browse files
sberyozkingsmet
authored andcommitted
Ensure the refreshed CSRF cookie retains the original value
(cherry picked from commit 2a872fe)
1 parent d61e59a commit 25ee2c7

File tree

2 files changed

+43
-6
lines changed

2 files changed

+43
-6
lines changed

extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,26 @@ public void filter(ResteasyReactiveContainerRequestContext requestContext, Routi
8787
if (requestMethodIsSafe(requestContext)) {
8888
// safe HTTP method, tolerate the absence of a token
8989
if (isCsrfTokenRequired(routing, config)) {
90-
// Set the CSRF cookie with a randomly generated value
91-
byte[] tokenBytes = new byte[config.tokenSize];
92-
secureRandom.nextBytes(tokenBytes);
93-
routing.put(CSRF_TOKEN_BYTES_KEY, tokenBytes);
94-
routing.put(CSRF_TOKEN_KEY, Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes));
95-
90+
if (cookieToken == null) {
91+
generateNewCsrfToken(routing, config);
92+
} else {
93+
String csrfTokenHeaderParam = requestContext.getHeaderString(config.tokenHeaderName);
94+
if (csrfTokenHeaderParam != null) {
95+
LOG.debugf("CSRF token found in the token header");
96+
// Verify the header, make sure the header value, possibly signed, is returned as the next cookie value
97+
verifyCsrfToken(requestContext, routing, config, cookieToken, csrfTokenHeaderParam);
98+
} else if (!config.tokenSignatureKey.isEmpty()) {
99+
// If the signature is required, then we can not use the current cookie value
100+
// as the HTML form token key because it represents a signed value of the previous key
101+
// and it will lead to the double-signing issue if this value is reused as the key.
102+
// It should be fine for simple HTML forms anyway
103+
generateNewCsrfToken(routing, config);
104+
} else {
105+
// Make sure the same cookie value is returned
106+
routing.put(CSRF_TOKEN_KEY, cookieToken);
107+
routing.put(CSRF_TOKEN_BYTES_KEY, Base64.getUrlDecoder().decode(cookieToken));
108+
}
109+
}
96110
routing.put(NEW_COOKIE_REQUIRED, true);
97111
}
98112
} else if (config.verifyToken) {
@@ -139,6 +153,14 @@ public void filter(ResteasyReactiveContainerRequestContext requestContext, Routi
139153
}
140154
}
141155

156+
private void generateNewCsrfToken(RoutingContext routing, CsrfReactiveConfig config) {
157+
// Set the CSRF cookie with a randomly generated value
158+
byte[] tokenBytes = new byte[config.tokenSize];
159+
secureRandom.nextBytes(tokenBytes);
160+
routing.put(CSRF_TOKEN_BYTES_KEY, tokenBytes);
161+
routing.put(CSRF_TOKEN_KEY, Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes));
162+
}
163+
142164
private void verifyCsrfToken(ResteasyReactiveContainerRequestContext requestContext, RoutingContext routing,
143165
CsrfReactiveConfig config, String cookieToken, String csrfToken) {
144166
if (cookieToken == null) {
@@ -160,6 +182,7 @@ private void verifyCsrfToken(ResteasyReactiveContainerRequestContext requestCont
160182
return;
161183
} else {
162184
routing.put(CSRF_TOKEN_KEY, csrfToken);
185+
routing.put(CSRF_TOKEN_BYTES_KEY, Base64.getUrlDecoder().decode(csrfToken));
163186
routing.put(CSRF_TOKEN_VERIFIED, true);
164187
// reset the cookie
165188
routing.put(NEW_COOKIE_REQUIRED, true);

integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static org.junit.jupiter.api.Assertions.assertEquals;
55
import static org.junit.jupiter.api.Assertions.assertNotNull;
66
import static org.junit.jupiter.api.Assertions.assertNull;
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
78
import static org.junit.jupiter.api.Assertions.fail;
89

910
import java.net.URL;
@@ -56,6 +57,8 @@ public void testCsrfTokenInForm() throws Exception {
5657
assertNotNull(htmlPage.getWebResponse().getResponseHeaderValue("Set-Cookie"));
5758
assertEquals("alice:true:tokenHeaderIsSet=false", textPage.getContent());
5859

60+
Cookie cookie1 = webClient.getCookieManager().getCookie("csrftoken");
61+
5962
// This request which returns String is not CSRF protected
6063
textPage = webClient.getPage("http://localhost:8081/service/hello");
6164
assertEquals("hello", textPage.getContent());
@@ -67,6 +70,11 @@ public void testCsrfTokenInForm() throws Exception {
6770
assertNotNull(htmlPage.getWebResponse().getResponseHeaderValue("Set-Cookie"));
6871
assertEquals("alice:true:tokenHeaderIsSet=false", textPage.getContent());
6972

73+
Cookie cookie2 = webClient.getCookieManager().getCookie("csrftoken");
74+
75+
assertEquals(cookie1.getValue(), cookie2.getValue());
76+
assertTrue(cookie1.getExpires().before(cookie2.getExpires()));
77+
7078
webClient.getCookieManager().clearCookies();
7179
}
7280
}
@@ -366,6 +374,12 @@ private void assurePostFormPath(io.vertx.ext.web.client.WebClient vertxWebClient
366374
if (responseBody != null) {
367375
assertEquals(responseBody, result.result().bodyAsString(), path);
368376
}
377+
if (expectedStatus != 400) {
378+
String[] nextCookie = result.result().cookies().get(0).split(";");
379+
String[] cookieNameValue = nextCookie[0].trim().split("=");
380+
assertEquals(csrfCookie.getName(), cookieNameValue[0]);
381+
assertEquals(csrfCookie.getValue(), cookieNameValue[1]);
382+
}
369383
}
370384

371385
private void assurePostJsonPath(io.vertx.ext.web.client.WebClient vertxWebClient, String path,

0 commit comments

Comments
 (0)