1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.cms.security;
35
36 import info.magnolia.audit.AuditLoggingUtil;
37 import info.magnolia.cms.filters.AbstractMgnlFilter;
38 import info.magnolia.context.Context;
39
40 import java.io.IOException;
41 import java.security.SecureRandom;
42 import java.util.Arrays;
43 import java.util.Base64;
44 import java.util.Optional;
45 import java.util.Random;
46 import java.util.Objects;
47
48 import javax.inject.Inject;
49 import javax.inject.Provider;
50 import javax.servlet.FilterChain;
51 import javax.servlet.ServletException;
52 import javax.servlet.http.Cookie;
53 import javax.servlet.http.HttpServletRequest;
54 import javax.servlet.http.HttpServletResponse;
55 import javax.servlet.http.HttpSession;
56
57 import org.apache.commons.lang3.StringUtils;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88 public class CsrfTokenSecurityFilter extends AbstractMgnlFilter {
89
90 private static final Logger log = LoggerFactory.getLogger(CsrfTokenSecurityFilter.class);
91
92 static final String CSRF_ATTRIBUTE_NAME = "csrf";
93 private static final String EVENT_TYPE = "Possible CSRF Attack";
94
95 private final Random random = new SecureRandom();
96
97 private final Provider<Context> contextProvider;
98
99 @Inject
100 public CsrfTokenSecurityFilter(final Provider<Context> contextProvider) {
101 this.contextProvider = contextProvider;
102 }
103
104 @Override
105 public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
106 if (csrfCheckPasses(request, response)) {
107 chain.doFilter(request, response);
108 }
109 }
110
111
112
113
114 protected boolean csrfCheckPasses(HttpServletRequest request, HttpServletResponse response) throws IOException {
115 HttpSession session = request.getSession(false);
116 return unloggedRequestCheckPasses(request, response, session) || loggedInRequestCheckPasses(request, response, session);
117 }
118
119
120
121
122
123
124
125 private boolean loggedInRequestCheckPasses(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {
126 if (session != null) {
127
128 if (request.getMethod().equals("POST") && !session.isNew()) {
129 String token = request.getParameter(CSRF_ATTRIBUTE_NAME);
130 if (StringUtils.isBlank(token)) {
131 csrfTokenMissing(request, response, request.getServletPath());
132 return false;
133 }
134 if (!token.equals(session.getAttribute(CSRF_ATTRIBUTE_NAME))) {
135 csrfTokenMismatch(request, response, request.getServletPath());
136 return false;
137 }
138
139 } else if (session.getAttribute(CSRF_ATTRIBUTE_NAME) == null) {
140 session.setAttribute(CSRF_ATTRIBUTE_NAME, generateSafeToken());
141 }
142 return true;
143 }
144 return false;
145 }
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160 private boolean unloggedRequestCheckPasses(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {
161 if (session == null) {
162
163 if (request.getMethod().equals("GET")) {
164 String token = generateSafeToken();
165 request.setAttribute(CSRF_ATTRIBUTE_NAME, token);
166
167 Cookie cookie = new Cookie(CSRF_ATTRIBUTE_NAME, token);
168 cookie.setPath(request.getServletPath());
169
170 cookie.setMaxAge(-1);
171 response.addCookie(cookie);
172
173
174 } else if (request.getMethod().equals("POST")) {
175 String token = request.getParameter(CSRF_ATTRIBUTE_NAME);
176 Optional<Cookie> cookie = Arrays.stream(request.getCookies())
177 .filter(c -> c.getName().equals(CSRF_ATTRIBUTE_NAME))
178 .findFirst();
179
180 if (StringUtils.isNotBlank(token) && cookie.isPresent()) {
181 if (!Objects.equals(token, cookie.get().getValue())) {
182 csrfTokenMismatch(request, response, request.getServletPath());
183 return false;
184 }
185 } else {
186 csrfTokenMissing(request, response, request.getServletPath());
187 return false;
188 }
189 }
190 return true;
191 }
192 return false;
193 }
194
195 private void csrfTokenMissing(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
196 final String auditDetails = String.format("CSRF token not set while user '%s' attempted to access url '%s'.", contextProvider.get().getUser().getName(), url);
197 handleError(request, response, auditDetails);
198 }
199
200 private void csrfTokenMismatch(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
201 final String auditDetails = String.format("CSRF token mismatched while user '%s' attempted to access url '%s'.", contextProvider.get().getUser().getName(), url);
202 handleError(request, response, auditDetails);
203 }
204
205
206
207
208
209 protected void handleError(HttpServletRequest request, HttpServletResponse response, String message) throws IOException {
210 auditLogging(request, response, message);
211 response.sendError(HttpServletResponse.SC_FORBIDDEN, "CSRF token mismatch possibly caused by expired session. Please re-open the page and submit the form again.");
212 }
213
214 private void auditLogging(HttpServletRequest request, HttpServletResponse response, String auditDetails) throws IOException {
215 log.warn("{}. {}", new Object[]{EVENT_TYPE, auditDetails});
216 AuditLoggingUtil.logSecurity(request.getRemoteAddr(), EVENT_TYPE, auditDetails);
217 }
218
219 private String generateSafeToken() {
220 byte bytes[] = new byte[20];
221 random.nextBytes(bytes);
222 Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
223 String token = encoder.encodeToString(bytes);
224 return token;
225 }
226
227 }