Skip to content

Commit a65ef4e

Browse files
committed
XSRF: add ignored methods + parameter handling for tokens
1 parent 9ceabf5 commit a65ef4e

File tree

4 files changed

+110
-66
lines changed

4 files changed

+110
-66
lines changed

web/security/src/main/java/org/seedstack/seed/web/security/WebSecurityConfig.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
package org.seedstack.seed.web.security;
1010

11+
import com.google.common.collect.Lists;
1112
import java.util.ArrayList;
1213
import java.util.Arrays;
1314
import java.util.Collections;
@@ -145,9 +146,12 @@ public static class XSRFConfig {
145146
@NotBlank
146147
private String headerName = "X-XSRF-TOKEN";
147148
@NotBlank
149+
private String paramName = "xsrfToken";
150+
@NotBlank
148151
private String algorithm = "SHA1PRNG";
149152
private int length = 32;
150153
private boolean perRequestToken = false;
154+
private List<String> ignoreHttpMethods = Lists.newArrayList("GET", "HEAD", "OPTIONS");
151155

152156
public String getCookieName() {
153157
return cookieName;
@@ -220,5 +224,23 @@ public XSRFConfig setCookieHttpOnly(boolean cookieHttpOnly) {
220224
this.cookieHttpOnly = cookieHttpOnly;
221225
return this;
222226
}
227+
228+
public String getParamName() {
229+
return paramName;
230+
}
231+
232+
public XSRFConfig setParamName(String paramName) {
233+
this.paramName = paramName;
234+
return this;
235+
}
236+
237+
public List<String> getIgnoreHttpMethods() {
238+
return ignoreHttpMethods;
239+
}
240+
241+
public XSRFConfig setIgnoreHttpMethods(List<String> ignoreHttpMethods) {
242+
this.ignoreHttpMethods = ignoreHttpMethods;
243+
return this;
244+
}
223245
}
224246
}

web/security/src/main/java/org/seedstack/seed/web/security/internal/AntiXsrfFilter.java

Lines changed: 64 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
package org.seedstack.seed.web.security.internal;
1010

1111
import java.security.SecureRandom;
12+
import java.util.List;
1213
import javax.servlet.ServletRequest;
1314
import javax.servlet.ServletResponse;
1415
import javax.servlet.http.Cookie;
@@ -35,56 +36,83 @@ protected boolean onPreHandle(ServletRequest request, ServletResponse response,
3536
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
3637
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
3738
final HttpSession session = httpServletRequest.getSession(false);
38-
final boolean noCheck;
39-
if (mappedValue != null && ((String[]) mappedValue).length != 0) {
40-
noCheck = NO_CHECK.equals(((String[]) mappedValue)[0]);
41-
} else {
42-
noCheck = false;
43-
}
4439

4540
// Only apply XSRF protection when there is a session
4641
if (session != null) {
4742
// If session is new, generate a token and put it in a cookie
4843
if (session.isNew()) {
4944
setXsrfCookie(httpServletResponse);
5045
}
51-
// Else, if noCheck is NOT set, check if the request and cookie tokens match
52-
else if (!noCheck) {
53-
String cookieToken = extractCookieToken(httpServletRequest);
54-
String requestToken = httpServletRequest.getHeader(xsrfConfig.getHeaderName());
55-
56-
if (requestToken == null) {
57-
((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN,
58-
"Missing CSRF protection token in the request headers");
59-
return false;
46+
// Else, apply XSRF protection logic
47+
else {
48+
final boolean noCheck;
49+
if (mappedValue != null && ((String[]) mappedValue).length != 0) {
50+
noCheck = NO_CHECK.equals(((String[]) mappedValue)[0]);
51+
} else {
52+
noCheck = false;
6053
}
6154

62-
if (cookieToken == null) {
63-
((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN,
64-
"Missing CSRF protection token cookie");
65-
return false;
55+
if (!noCheck && !isRequestIgnored(httpServletRequest)) {
56+
String cookieToken = getTokenFromCookie(httpServletRequest);
57+
58+
// If no cookie is available, send an error
59+
if (cookieToken == null) {
60+
((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN,
61+
"Missing CSRF protection token cookie");
62+
return false;
63+
}
64+
65+
// Try to obtain the request token from a header
66+
String requestToken = getTokenFromHeader(httpServletRequest);
67+
68+
// Fallback to query parameter if we didn't a token in the headers
69+
if (requestToken == null) {
70+
requestToken = getTokenFromParameter(httpServletRequest);
71+
}
72+
73+
// If no request token available, send an error
74+
if (requestToken == null) {
75+
((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN,
76+
"Missing CSRF protection token in the request headers");
77+
return false;
78+
}
79+
80+
// If tokens don't match, send an error
81+
if (!cookieToken.equals(requestToken)) {
82+
((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN,
83+
"Request token does not match session token");
84+
return false;
85+
}
86+
87+
// Regenerate token if per-request tokens are in use
88+
if (xsrfConfig.isPerRequestToken()) {
89+
setXsrfCookie(httpServletResponse);
90+
}
6691
}
92+
}
93+
}
94+
return true;
95+
}
6796

68-
// Check for multiple headers (keep only the first one)
69-
int commaIndex = requestToken.indexOf(',');
70-
if (commaIndex != -1) {
71-
requestToken = requestToken.substring(0, commaIndex).trim();
72-
}
97+
protected boolean isRequestIgnored(HttpServletRequest httpServletRequest) {
98+
List<String> ignoreHttpMethods = xsrfConfig.getIgnoreHttpMethods();
99+
return ignoreHttpMethods != null && ignoreHttpMethods.contains(httpServletRequest.getMethod());
100+
}
73101

74-
// Check if tokens match
75-
if (!cookieToken.equals(requestToken)) {
76-
((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN,
77-
"Request token does not match session token");
78-
return false;
79-
}
102+
protected String getTokenFromParameter(HttpServletRequest httpServletRequest) {
103+
return httpServletRequest.getParameter(xsrfConfig.getParamName());
104+
}
80105

81-
// Regenerate token if per-request tokens are in use
82-
if (xsrfConfig.isPerRequestToken()) {
83-
setXsrfCookie(httpServletResponse);
84-
}
106+
protected String getTokenFromHeader(HttpServletRequest httpServletRequest) {
107+
String header = httpServletRequest.getHeader(xsrfConfig.getHeaderName());
108+
if (header != null) {
109+
int commaIndex = header.indexOf(',');
110+
if (commaIndex != -1) {
111+
// If header is multi-valued, only keep the first one
112+
header = header.substring(0, commaIndex).trim();
85113
}
86114
}
87-
return true;
115+
return header;
88116
}
89117

90118
protected void postHandle(ServletRequest request, ServletResponse response) {
@@ -117,7 +145,7 @@ protected void deleteXsrfCookie(HttpServletResponse httpServletResponse) {
117145
httpServletResponse.setHeader(SET_COOKIE_HEADER, cookieSpec);
118146
}
119147

120-
protected String extractCookieToken(HttpServletRequest httpServletRequest) {
148+
protected String getTokenFromCookie(HttpServletRequest httpServletRequest) {
121149
String cookieName = xsrfConfig.getCookieName();
122150
for (Cookie cookie : httpServletRequest.getCookies()) {
123151
if (cookieName.equals(cookie.getName())) {

web/security/src/main/resources/org/seedstack/seed/web/security/WebSecurityConfig.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ loginUrl=The URL of the authentication form to redirect to when the user needs t
1414
logoutUrl=The URL to redirect to after logout.
1515
xsrf.cookieName=The name of the cookie used for XSRF protection.
1616
xsrf.headerName=The name of the HTTP header used for XSRF protection.
17+
xsrf.paramName=The name of the HTTP parameter (query param or form param) used for XSRF protection.
1718
xsrf.algorithm=The name of the SecureRandom algorithm for generating the XSRF random token.
1819
xsrf.length=The length of the random XSRF token.
1920
xsrf.perRequestToken=If true, a new random XSRF token is generated for each request.
2021
xsrf.sameSitePolicy=Define the value of the 'SameSite' attribute on the XSRF cookie.
2122
xsrf.httpOnly=If true, the XSRF token cookie will be set to HTTP only, preventing its access from JavaScript.
23+
xsrf.ignoreHttpMethods=The list of HTTP methods ignored for XSRF protection.
2224
form.usernameParameter=The name of the parameter carrying the user name in the authentication form.
2325
form.passwordParameter=The name of the parameter carrying the password in the authentication form.
2426
form.rememberMeParameter=The name of the parameter carrying the remember me flag in the authentication form.

web/security/src/test/resources/application.yaml

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -36,38 +36,30 @@ security:
3636
loginUrl: /login.html
3737
logoutUrl: /logout.html
3838
successUrl: /success.html
39+
xsrf:
40+
ignoreHttpMethods: []
3941
form:
4042
usernameParameter: user
4143
passwordParameter: pw
4244
urls:
43-
-
44-
pattern: /jediCouncil.html
45-
filters: [authcBasic, 'perms[lightSaber:wield, academy:learn]']
46-
-
47-
pattern: /jediAcademy.html
48-
filters: [authcBasic, 'perms[academy:learn]']
49-
-
50-
pattern: /protected
51-
filters: authc
52-
-
53-
pattern: /cert-protected
54-
filters: cert
55-
-
56-
pattern: /login.html
57-
filters: authc
58-
-
59-
pattern: /success.html
60-
filters: authc
61-
-
62-
pattern: /logout
63-
filters: logout
64-
-
65-
pattern: /teapot
66-
filters: 'teapot[param]'
67-
-
68-
pattern: /xsrf-protected-without-session
69-
filters: xsrf
70-
-
71-
pattern: /xsrf-protected-with-session
72-
filters: [authcBasic, xsrf]
45+
- pattern: /jediCouncil.html
46+
filters: [authcBasic, 'perms[lightSaber:wield, academy:learn]']
47+
- pattern: /jediAcademy.html
48+
filters: [authcBasic, 'perms[academy:learn]']
49+
- pattern: /protected
50+
filters: authc
51+
- pattern: /cert-protected
52+
filters: cert
53+
- pattern: /login.html
54+
filters: authc
55+
- pattern: /success.html
56+
filters: authc
57+
- pattern: /logout
58+
filters: logout
59+
- pattern: /teapot
60+
filters: 'teapot[param]'
61+
- pattern: /xsrf-protected-without-session
62+
filters: xsrf
63+
- pattern: /xsrf-protected-with-session
64+
filters: [authcBasic, xsrf]
7365

0 commit comments

Comments
 (0)