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
47 import javax.inject.Inject;
48 import javax.inject.Provider;
49 import javax.servlet.FilterChain;
50 import javax.servlet.ServletException;
51 import javax.servlet.http.Cookie;
52 import javax.servlet.http.HttpServletRequest;
53 import javax.servlet.http.HttpServletResponse;
54 import javax.servlet.http.HttpSession;
55
56 import org.apache.commons.lang3.StringUtils;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
59
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 public class CsrfTokenSecurityFilter extends AbstractMgnlFilter {
88
89 private static final Logger log = LoggerFactory.getLogger(CsrfTokenSecurityFilter.class);
90
91 static final String CSRF_ATTRIBUTE_NAME = "csrf";
92 private static final String EVENT_TYPE = "Possible CSRF Attack";
93
94 private Random random = new SecureRandom();
95
96 private final Provider<Context> contextProvider;
97
98 @Inject
99 public CsrfTokenSecurityFilter(final Provider<Context> contextProvider) {
100 this.contextProvider = contextProvider;
101 }
102
103 @Override
104 public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
105 if (csrfCheckPasses(request, response)) {
106 chain.doFilter(request, response);
107 }
108 }
109
110
111
112
113 protected boolean csrfCheckPasses(HttpServletRequest request, HttpServletResponse response) throws IOException {
114 HttpSession session = request.getSession(false);
115 return unloggedRequestCheckPasses(request, response, session) || loggedInRequestCheckPasses(request, response, session);
116 }
117
118
119
120
121
122
123
124 private boolean loggedInRequestCheckPasses(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {
125 if (session != null) {
126
127 if (request.getMethod().equals("POST") && !session.isNew()) {
128 String token = request.getParameter(CSRF_ATTRIBUTE_NAME);
129 if (StringUtils.isBlank(token)) {
130 csrfTokenMissing(request, response, request.getServletPath());
131 return false;
132 }
133 if (!token.equals(session.getAttribute(CSRF_ATTRIBUTE_NAME))) {
134 csrfTokenMismatch(request, response, request.getServletPath());
135 return false;
136 }
137
138 } else if (session.getAttribute(CSRF_ATTRIBUTE_NAME) == null) {
139 session.setAttribute(CSRF_ATTRIBUTE_NAME, generateSafeToken());
140 }
141 return true;
142 }
143 return false;
144 }
145
146
147
148
149
150
151
152
153
154
155
156
157 private boolean unloggedRequestCheckPasses(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {
158 if (session == null) {
159
160 if (request.getMethod().equals("GET")) {
161 String token = generateSafeToken();
162 request.setAttribute(CSRF_ATTRIBUTE_NAME, token);
163
164 Cookie cookie = new Cookie(CSRF_ATTRIBUTE_NAME, SecurityUtil.getBCrypt(token));
165 cookie.setPath(request.getServletPath());
166
167 cookie.setMaxAge(-1);
168 response.addCookie(cookie);
169
170
171 } else if (request.getMethod().equals("POST")) {
172 String token = request.getParameter(CSRF_ATTRIBUTE_NAME);
173 Optional<Cookie> cookie = Arrays.stream(request.getCookies())
174 .filter(c -> c.getName().equals(CSRF_ATTRIBUTE_NAME))
175 .findFirst();
176
177 if (StringUtils.isNotBlank(token) && cookie.isPresent()) {
178 if (!SecurityUtil.matchBCrypted(token, cookie.get().getValue())) {
179 csrfTokenMismatch(request, response, request.getServletPath());
180 return false;
181 }
182 } else {
183 csrfTokenMissing(request, response, request.getServletPath());
184 return false;
185 }
186 }
187 return true;
188 }
189 return false;
190 }
191
192 private void csrfTokenMissing(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
193 final String auditDetails = String.format("CSRF token not set while user '%s' attempted to access url '%s'.", contextProvider.get().getUser().getName(), url);
194 handleError(request, response, auditDetails);
195 }
196
197 private void csrfTokenMismatch(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
198 final String auditDetails = String.format("CSRF token mismatched while user '%s' attempted to access url '%s'.", contextProvider.get().getUser().getName(), url);
199 handleError(request, response, auditDetails);
200 }
201
202
203
204
205
206 protected void handleError(HttpServletRequest request, HttpServletResponse response, String message) throws IOException {
207 auditLogging(request, response, message);
208 response.sendError(HttpServletResponse.SC_FORBIDDEN, "CSRF token mismatch possibly caused by expired session. Please re-open the page and submit the form again.");
209 }
210
211 private void auditLogging(HttpServletRequest request, HttpServletResponse response, String auditDetails) throws IOException {
212 log.warn("{}. {}", new Object[]{EVENT_TYPE, auditDetails});
213 AuditLoggingUtil.logSecurity(request.getRemoteAddr(), EVENT_TYPE, auditDetails);
214 }
215
216 private String generateSafeToken() {
217 byte bytes[] = new byte[20];
218 random.nextBytes(bytes);
219 Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
220 String token = encoder.encodeToString(bytes);
221 return token;
222 }
223
224 }